@elabs-ai/components-viewer 4.2.0 → 5.0.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/dist/{code-adapter-ADZ4UOGN.js → adapters/code/code-adapter.js} +15 -64
- package/dist/adapters/code/code-adapter.js.map +1 -0
- package/dist/adapters/code/code-language.js +10 -0
- package/dist/adapters/code/code-language.js.map +1 -0
- package/dist/adapters/code/code-manifest.js +10 -0
- package/dist/adapters/code/code-manifest.js.map +1 -0
- package/dist/adapters/code/code-theme.js +8 -0
- package/dist/adapters/code/code-theme.js.map +1 -0
- package/dist/{csv-adapter-7HOFZ5Z6.js → adapters/csv/csv-adapter.js} +13 -9
- package/dist/adapters/csv/csv-adapter.js.map +1 -0
- package/dist/adapters/csv/csv-manifest.js +9 -0
- package/dist/adapters/csv/csv-manifest.js.map +1 -0
- package/dist/{docx-adapter-5CQDHWTD.js → adapters/docx/docx-adapter.js} +17 -175
- package/dist/adapters/docx/docx-adapter.js.map +1 -0
- package/dist/adapters/docx/docx-manifest.js +9 -0
- package/dist/adapters/docx/docx-manifest.js.map +1 -0
- package/dist/adapters/docx/docx-model.js +21 -0
- package/dist/adapters/docx/docx-model.js.map +1 -0
- package/dist/{image-adapter-WOHZR24J.js → adapters/image/image-adapter.js} +4 -3
- package/dist/adapters/image/image-adapter.js.map +1 -0
- package/dist/adapters/image/image-manifest.js +9 -0
- package/dist/adapters/image/image-manifest.js.map +1 -0
- package/dist/adapters/index.js +56 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/{json-adapter-ZUW5GQHE.js → adapters/json/json-adapter.js} +4 -3
- package/dist/adapters/json/json-adapter.js.map +1 -0
- package/dist/adapters/json/json-manifest.js +9 -0
- package/dist/adapters/json/json-manifest.js.map +1 -0
- package/dist/{markdown-adapter-YC6WTBS4.js → adapters/markdown/markdown-adapter.js} +14 -36
- package/dist/adapters/markdown/markdown-adapter.js.map +1 -0
- package/dist/adapters/markdown/markdown-manifest.js +9 -0
- package/dist/adapters/markdown/markdown-manifest.js.map +1 -0
- package/dist/adapters/markdown/markdown-marks.js +10 -0
- package/dist/adapters/markdown/markdown-marks.js.map +1 -0
- package/dist/{media-adapter-MCTB4GBH.js → adapters/media/media-adapter.js} +4 -3
- package/dist/adapters/media/media-adapter.js.map +1 -0
- package/dist/adapters/media/media-manifest.js +9 -0
- package/dist/adapters/media/media-manifest.js.map +1 -0
- package/dist/{pdf-adapter-5PMKEXUD.js → adapters/pdf/pdf-adapter.js} +31 -81
- package/dist/adapters/pdf/pdf-adapter.js.map +1 -0
- package/dist/adapters/pdf/pdf-engine.js +14 -0
- package/dist/adapters/pdf/pdf-engine.js.map +1 -0
- package/dist/adapters/pdf/pdf-manifest.js +9 -0
- package/dist/adapters/pdf/pdf-manifest.js.map +1 -0
- package/dist/adapters/pdf/pdf-text.js +20 -0
- package/dist/adapters/pdf/pdf-text.js.map +1 -0
- package/dist/{pptx-adapter-BONWFHYT.js → adapters/pptx/pptx-adapter.js} +34 -135
- package/dist/adapters/pptx/pptx-adapter.js.map +1 -0
- package/dist/adapters/pptx/pptx-manifest.js +9 -0
- package/dist/adapters/pptx/pptx-manifest.js.map +1 -0
- package/dist/adapters/pptx/pptx-model.js +37 -0
- package/dist/adapters/pptx/pptx-model.js.map +1 -0
- package/dist/{text-adapter-NFNWB5W3.js → adapters/text/text-adapter.js} +8 -5
- package/dist/adapters/text/text-adapter.js.map +1 -0
- package/dist/adapters/text/text-manifest.js +9 -0
- package/dist/adapters/text/text-manifest.js.map +1 -0
- package/dist/{xlsx-adapter-O3XIVLFC.js → adapters/xlsx/xlsx-adapter.js} +12 -8
- package/dist/adapters/xlsx/xlsx-adapter.js.map +1 -0
- package/dist/adapters/xlsx/xlsx-manifest.js +9 -0
- package/dist/adapters/xlsx/xlsx-manifest.js.map +1 -0
- package/dist/chunk-3HKQFFZW.js +31 -0
- package/dist/chunk-3HKQFFZW.js.map +1 -0
- package/dist/chunk-5VJWILZ7.js +182 -0
- package/dist/chunk-5VJWILZ7.js.map +1 -0
- package/dist/{chunk-UL43NGUG.js → chunk-5YE2MUS2.js} +1 -1
- package/dist/{chunk-5OMONA2G.js → chunk-6FVJ4D7W.js} +3 -15
- package/dist/chunk-6FVJ4D7W.js.map +1 -0
- package/dist/{chunk-RGWDIQJR.js → chunk-7EASJBQM.js} +2 -2
- package/dist/chunk-7PGGKQVY.js +26 -0
- package/dist/chunk-7PGGKQVY.js.map +1 -0
- package/dist/{chunk-XBOR2YXS.js → chunk-AK7F3IZE.js} +9 -30
- package/dist/chunk-AK7F3IZE.js.map +1 -0
- package/dist/chunk-AOYEL7O3.js +34 -0
- package/dist/chunk-AOYEL7O3.js.map +1 -0
- package/dist/chunk-BDUFH66C.js +61 -0
- package/dist/chunk-BDUFH66C.js.map +1 -0
- package/dist/chunk-BWMXMHVZ.js +107 -0
- package/dist/chunk-BWMXMHVZ.js.map +1 -0
- package/dist/chunk-CIT2HHRI.js +9 -0
- package/dist/chunk-CIT2HHRI.js.map +1 -0
- package/dist/chunk-CLOI4HBA.js +18 -0
- package/dist/chunk-CLOI4HBA.js.map +1 -0
- package/dist/chunk-CMYBLW7D.js +21 -0
- package/dist/chunk-CMYBLW7D.js.map +1 -0
- package/dist/chunk-CO2VJRSU.js +18 -0
- package/dist/chunk-CO2VJRSU.js.map +1 -0
- package/dist/{chunk-3U3MESBU.js → chunk-EA72HA4I.js} +2 -2
- package/dist/{chunk-KOMDRWOU.js → chunk-FDT4QOIG.js} +2 -2
- package/dist/chunk-FQHZEQUH.js +90 -0
- package/dist/chunk-FQHZEQUH.js.map +1 -0
- package/dist/chunk-G27UJZWH.js +82 -0
- package/dist/chunk-G27UJZWH.js.map +1 -0
- package/dist/{chunk-BCF3RWRL.js → chunk-GH4UBYST.js} +2 -2
- package/dist/{chunk-2NQ4RSJ3.js → chunk-GNBD53AT.js} +2 -2
- package/dist/{chunk-VO2273Z2.js → chunk-HOQZ7OPH.js} +2 -18
- package/dist/chunk-HOQZ7OPH.js.map +1 -0
- package/dist/{chunk-GGR7BNW4.js → chunk-HUUJIFFT.js} +2 -2
- package/dist/{chunk-FELIAGKI.js → chunk-I5XD2NJ6.js} +2 -2
- package/dist/chunk-IB4N72FA.js +21 -0
- package/dist/chunk-IB4N72FA.js.map +1 -0
- package/dist/{chunk-4OFQYALG.js → chunk-IQ6Z4X3K.js} +8 -68
- package/dist/chunk-IQ6Z4X3K.js.map +1 -0
- package/dist/{chunk-AORNMH77.js → chunk-JHFC2KBI.js} +1 -5
- package/dist/chunk-JHFC2KBI.js.map +1 -0
- package/dist/chunk-LRPZ373O.js +93 -0
- package/dist/chunk-LRPZ373O.js.map +1 -0
- package/dist/chunk-M77ICVWP.js +24 -0
- package/dist/chunk-M77ICVWP.js.map +1 -0
- package/dist/chunk-MJUH32ZK.js +24 -0
- package/dist/chunk-MJUH32ZK.js.map +1 -0
- package/dist/chunk-PAIVK4N6.js +162 -0
- package/dist/chunk-PAIVK4N6.js.map +1 -0
- package/dist/chunk-PKYDKXCO.js +129 -0
- package/dist/chunk-PKYDKXCO.js.map +1 -0
- package/dist/chunk-QIPKBOZJ.js +59 -0
- package/dist/chunk-QIPKBOZJ.js.map +1 -0
- package/dist/chunk-SASY77ZL.js +29 -0
- package/dist/chunk-SASY77ZL.js.map +1 -0
- package/dist/{chunk-SLXRUYT3.js → chunk-SBG7WPGO.js} +2 -44
- package/dist/chunk-SBG7WPGO.js.map +1 -0
- package/dist/chunk-T4Y4YRHJ.js +733 -0
- package/dist/chunk-T4Y4YRHJ.js.map +1 -0
- package/dist/chunk-TLCNGQKL.js +37 -0
- package/dist/chunk-TLCNGQKL.js.map +1 -0
- package/dist/chunk-US5KT3TR.js +141 -0
- package/dist/chunk-US5KT3TR.js.map +1 -0
- package/dist/{chunk-HMG2ERXH.js → chunk-YA72QE57.js} +2 -2
- package/dist/{chunk-H3OAON3D.js → chunk-YWHFMCS4.js} +2 -2
- package/dist/{chunk-UJIJECEF.js → chunk-ZAVXZRIO.js} +2 -2
- package/dist/components/grid-text.js +19 -0
- package/dist/components/grid-text.js.map +1 -0
- package/dist/components/marked-text.js +10 -0
- package/dist/components/marked-text.js.map +1 -0
- package/dist/components/sheet-table.js +14 -0
- package/dist/components/sheet-table.js.map +1 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/highlight-marks.js +10 -0
- package/dist/core/highlight-marks.js.map +1 -0
- package/dist/core/highlight-resolve.js +10 -0
- package/dist/core/highlight-resolve.js.map +1 -0
- package/dist/core/highlight.js +12 -0
- package/dist/core/highlight.js.map +1 -0
- package/dist/core/registry.js +12 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/scroll-host.js +10 -0
- package/dist/core/scroll-host.js.map +1 -0
- package/dist/core/text-index.js +14 -0
- package/dist/core/text-index.js.map +1 -0
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/use-highlight-scroll.js +11 -0
- package/dist/core/use-highlight-scroll.js.map +1 -0
- package/dist/core/use-page-control.js +9 -0
- package/dist/core/use-page-control.js.map +1 -0
- package/dist/core/use-paged-scroll.js +10 -0
- package/dist/core/use-paged-scroll.js.map +1 -0
- package/dist/core/use-viewport-size.js +10 -0
- package/dist/core/use-viewport-size.js.map +1 -0
- package/dist/core/zoom.js +16 -0
- package/dist/core/zoom.js.map +1 -0
- package/dist/file-viewer/file-viewer-context.js +11 -0
- package/dist/file-viewer/file-viewer-context.js.map +1 -0
- package/dist/file-viewer/file-viewer-find.js +13 -0
- package/dist/file-viewer/file-viewer-find.js.map +1 -0
- package/dist/file-viewer/file-viewer-pager.js +10 -0
- package/dist/file-viewer/file-viewer-pager.js.map +1 -0
- package/dist/file-viewer/file-viewer-zoom.js +13 -0
- package/dist/file-viewer/file-viewer-zoom.js.map +1 -0
- package/dist/file-viewer/file-viewer.js +48 -0
- package/dist/file-viewer/file-viewer.js.map +1 -0
- package/dist/index.js +78 -1274
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/file-viewer/file-viewer-recipes.stories.tsx +406 -0
- package/src/file-viewer/file-viewer.stories.tsx +2 -2
- package/dist/chunk-4OFQYALG.js.map +0 -1
- package/dist/chunk-5OMONA2G.js.map +0 -1
- package/dist/chunk-AORNMH77.js.map +0 -1
- package/dist/chunk-SLXRUYT3.js.map +0 -1
- package/dist/chunk-VO2273Z2.js.map +0 -1
- package/dist/chunk-XBOR2YXS.js.map +0 -1
- package/dist/code-adapter-ADZ4UOGN.js.map +0 -1
- package/dist/csv-adapter-7HOFZ5Z6.js.map +0 -1
- package/dist/docx-adapter-5CQDHWTD.js.map +0 -1
- package/dist/image-adapter-WOHZR24J.js.map +0 -1
- package/dist/json-adapter-ZUW5GQHE.js.map +0 -1
- package/dist/markdown-adapter-YC6WTBS4.js.map +0 -1
- package/dist/media-adapter-MCTB4GBH.js.map +0 -1
- package/dist/pdf-adapter-5PMKEXUD.js.map +0 -1
- package/dist/pptx-adapter-BONWFHYT.js.map +0 -1
- package/dist/text-adapter-NFNWB5W3.js.map +0 -1
- package/dist/xlsx-adapter-O3XIVLFC.js.map +0 -1
- /package/dist/{chunk-UL43NGUG.js.map → chunk-5YE2MUS2.js.map} +0 -0
- /package/dist/{chunk-RGWDIQJR.js.map → chunk-7EASJBQM.js.map} +0 -0
- /package/dist/{chunk-3U3MESBU.js.map → chunk-EA72HA4I.js.map} +0 -0
- /package/dist/{chunk-KOMDRWOU.js.map → chunk-FDT4QOIG.js.map} +0 -0
- /package/dist/{chunk-BCF3RWRL.js.map → chunk-GH4UBYST.js.map} +0 -0
- /package/dist/{chunk-2NQ4RSJ3.js.map → chunk-GNBD53AT.js.map} +0 -0
- /package/dist/{chunk-GGR7BNW4.js.map → chunk-HUUJIFFT.js.map} +0 -0
- /package/dist/{chunk-FELIAGKI.js.map → chunk-I5XD2NJ6.js.map} +0 -0
- /package/dist/{chunk-HMG2ERXH.js.map → chunk-YA72QE57.js.map} +0 -0
- /package/dist/{chunk-H3OAON3D.js.map → chunk-YWHFMCS4.js.map} +0 -0
- /package/dist/{chunk-UJIJECEF.js.map → chunk-ZAVXZRIO.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/code/code-adapter.tsx","../src/adapters/code/code-theme.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Code adapter — source files, highlighted with Shiki and coloured by tokens.\n *\n * Highlighting happens at LOAD, not at render: `load()` returns lines of plain\n * `{ text, color }` tokens, so the renderer draws data like every other adapter\n * and Shiki never reaches the render path. The colours are `var(--code-*)`\n * references (see `code-theme.ts`), which is why a theme switch recolours the\n * document without re-tokenizing it.\n *\n * The plain-text adapter is still the backstop. This one claims extensions it\n * has a grammar for; anything else — a log, a `.env`, no extension at all —\n * falls through to text, and so does every file when Shiki is not installed.\n */\n\nimport { MatchHighlight, cn, StatePanel, useLocale } from \"@elabs-ai/components-ui\";\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { Fragment, useMemo, useRef } from \"react\";\nimport type { BundledLanguage, ThemedToken } from \"shiki\";\n\nimport { toViewerError } from \"../../core/errors\";\nimport { localizeRanges, toMarkRanges } from \"../../core/highlight-marks\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { languageFor } from \"./code-language\";\nimport { codeManifest } from \"./code-manifest\";\nimport { codeViewerTheme } from \"./code-theme\";\n\n/**\n * Characters kept before truncation. Well below the plain-text limit because\n * this text is also TOKENIZED — a grammar pass over multiple megabytes blocks\n * the main thread, and no one reads a 400k-character file in a preview pane.\n */\nexport const CODE_CHARACTER_LIMIT = 400_000;\n\n/** One highlighted run. `color` is always a `var(--code-*)` reference. */\nexport interface CodeToken {\n text: string;\n color?: string;\n italic?: boolean;\n bold?: boolean;\n underline?: boolean;\n}\n\nexport interface CodeDocument extends AdapterDocument {\n kind: \"code\";\n /** Shiki language id, or `undefined` when the file rendered unhighlighted. */\n language?: string;\n /** One entry per line; a blank line is an empty array. */\n lines: CodeToken[][];\n text: string;\n /** Total characters in the file, when more than what is shown. */\n totalCharacters?: number;\n}\n\n// Shiki encodes font style as bitflags: 1 = italic, 2 = bold, 4 = underline.\nconst hasFontFlag = (fontStyle: number | undefined, flag: number) =>\n ((fontStyle ?? 0) & flag) === flag;\n\n/**\n * Highlighters, cached per language for the lifetime of the page.\n *\n * Not per-document state, so it deliberately outlives the adapter instance:\n * building one loads a grammar and the WASM engine, and paging through twenty\n * TypeScript files in a file tree should pay for that once.\n */\nconst highlighters = new Map<string, Promise<{ tokenize(code: string): ThemedToken[][] }>>();\n\nfunction getHighlighter(language: BundledLanguage) {\n let pending = highlighters.get(language);\n if (!pending) {\n pending = (async () => {\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const { createHighlighter } = await import(\"shiki\");\n const highlighter = await createHighlighter({\n themes: [codeViewerTheme],\n langs: [language],\n });\n return {\n tokenize: (code: string) =>\n highlighter.codeToTokens(code, {\n lang: language,\n theme: codeViewerTheme.name as string,\n }).tokens,\n };\n })().catch((error: unknown) => {\n // Not cached: a peer installed later, or a transient chunk-load failure,\n // must be retryable without a page reload.\n highlighters.delete(language);\n throw error;\n });\n highlighters.set(language, pending);\n }\n return pending;\n}\n\n/** Split unhighlighted text into the same line/token shape. */\nfunction plainLines(text: string): CodeToken[][] {\n return text.split(\"\\n\").map((line) => (line.length > 0 ? [{ text: line }] : []));\n}\n\nclass CodeAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<CodeDocument> {\n let raw: string;\n try {\n raw = await source.text(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n const truncated = raw.length > CODE_CHARACTER_LIMIT;\n const text = truncated ? raw.slice(0, CODE_CHARACTER_LIMIT) : raw;\n const totalCharacters = truncated ? raw.length : undefined;\n // So a passage the shell cannot find reads as \"past the part we previewed\"\n // rather than \"not in this document\".\n const textTruncated = truncated || undefined;\n const language = languageFor(source.name);\n\n // No grammar for this name — still a perfectly readable file.\n if (!language) {\n return { kind: \"code\", lines: plainLines(text), text, totalCharacters, textTruncated };\n }\n\n const highlighter = await getHighlighter(language);\n if (context.signal?.aborted) {\n return {\n kind: \"code\",\n language,\n lines: plainLines(text),\n text,\n totalCharacters,\n textTruncated,\n };\n }\n\n const lines = highlighter.tokenize(text).map((line) =>\n line.map((token) => ({\n text: token.content,\n color: token.color,\n italic: hasFontFlag(token.fontStyle, 1) || undefined,\n bold: hasFontFlag(token.fontStyle, 2) || undefined,\n underline: hasFontFlag(token.fontStyle, 4) || undefined,\n })),\n );\n\n return { kind: \"code\", language, lines, text, totalCharacters, textTruncated };\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Where each rendered line begins in `document.text`.\n *\n * Derived from the TEXT, not from the token lines: the text is the address space\n * every highlight is expressed in, so counting its newlines is the only mapping\n * that cannot drift from what a caller asked for. The two agree by construction\n * — the tokenizer splits on the same newlines — and `code-adapter.test.tsx`\n * locks that.\n */\nfunction lineStartsIn(text: string): number[] {\n const starts = [0];\n for (let i = 0; i < text.length; i++) {\n if (text[i] === \"\\n\") starts.push(i + 1);\n }\n return starts;\n}\n\nfunction CodeRenderer({\n document: doc,\n className,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const code = doc as CodeDocument;\n const { t, formatNumber } = useLocale();\n const container = useRef<HTMLDivElement>(null);\n const lineStarts = useMemo(() => lineStartsIn(code.text), [code.text]);\n const marks = useMemo(\n () => toMarkRanges(highlights, code.text.length),\n [highlights, code.text.length],\n );\n useScrollActiveHighlightIntoView(container, activeHighlightId);\n\n if (code.text.length === 0) {\n return <StatePanel kind=\"empty\" title={t(\"viewer.file.empty\")} className={className} />;\n }\n\n return (\n // No `overflow-auto` here: `FileViewerContent` is the scroll boundary, and\n // nesting a second one clips the last line above the outer pane's padding.\n <div ref={container} className={cn(\"flex flex-col gap-2\", className)}>\n {code.totalCharacters !== undefined && (\n // Not an error — the file is fine, we are showing part of it.\n <p role=\"status\" className=\"text-meta text-muted-foreground\">\n {t(\"viewer.text.truncated\", {\n shown: formatNumber(code.text.length),\n total: formatNumber(code.totalCharacters),\n })}\n </p>\n )}\n <pre className=\"text-code text-code-foreground font-mono\">\n <code className=\"grid grid-cols-[auto_1fr] gap-x-4\">\n {code.lines.map((tokens, lineIndex) => {\n let offset = lineStarts[lineIndex] ?? code.text.length;\n return (\n <Fragment key={lineIndex}>\n {/* Decorative: the numbers are a reading aid, and keeping them out\n of the accessibility tree also keeps them out of a copy. Not\n `formatNumber` — a line gutter is an ordinal, so a thousands\n separator at line 1,000 would be wrong in every locale. */}\n <span\n aria-hidden=\"true\"\n className=\"text-muted-foreground text-right tabular-nums select-none\"\n >\n {lineIndex + 1}\n </span>\n <span className=\"break-words whitespace-pre-wrap\">\n {tokens.map((token, tokenIndex) => {\n const start = offset;\n offset += token.text.length;\n // Syntax colour is inline `var(--code-*)` (see the module\n // header), and a mark's plate would fight it — so the mark\n // layer only wraps the tokens it actually touches, and every\n // other token keeps rendering exactly as before.\n const local = localizeRanges(marks, start, offset);\n const style = {\n color: token.color,\n fontStyle: token.italic ? \"italic\" : undefined,\n fontWeight: token.bold ? \"bold\" : undefined,\n textDecoration: token.underline ? \"underline\" : undefined,\n };\n return local.ranges.length === 0 ? (\n <span key={tokenIndex} style={style}>\n {token.text}\n </span>\n ) : (\n <MatchHighlight\n key={tokenIndex}\n style={style}\n text={token.text}\n ranges={local.ranges}\n activeIndex={local.activeIndex}\n />\n );\n })}\n </span>\n </Fragment>\n );\n })}\n </code>\n </pre>\n </div>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: codeManifest,\n create: () => new CodeAdapter(),\n Renderer: CodeRenderer,\n};\n\nexport default adapterModule;\n","/**\n * The Shiki theme the code adapter tokenizes with — brand tokens, not a\n * recoloured `github-light`.\n *\n * Every colour here is a `var(--code-*)` reference rather than a literal, which\n * is what makes ONE theme correct in EVERY brand theme: the active `data-theme`\n * block decides what `--code-keyword` resolves to, so a runtime theme switch\n * recolours already-tokenized code with no re-highlight, no `MutationObserver`\n * and no cache to invalidate. It is the same technique Shiki ships as\n * `createCssVariablesTheme`, written out by hand for one reason: that helper\n * maps a fixed, smaller scope set and cannot reach `--code-type` or\n * `--code-tag`, which this design system does define.\n *\n * `@elabs-ai/components-ai`'s `CodeBlock` resolves the same tokens\n * the other way round (oklch → hex, re-derived per theme change) because\n * `@streamdown/code` freezes its themes at import time and cannot take a live\n * `var()`. Nothing here needs that, so nothing here pays for it.\n *\n * Type-only import: this module is pure data and holds no edge to the engine.\n */\n\nimport type { ThemeRegistrationRaw } from \"shiki\";\n\n/**\n * TextMate scopes → brand code tokens.\n *\n * Ordered coarse-to-fine, because Shiki resolves the LAST matching rule: the\n * broad `constant` rule has to come before `constant.numeric` or every number\n * would read as a constant.\n *\n * `italic` is a convention, not a colour, so it is theme-safe — but it is\n * declared PER RULE and never inferred from the scope list. A rule's styling\n * applies to every scope in its array, so a lone italic scope sharing an array\n * with ordinary ones italicises all of them: bundling `entity.other.attribute-name`\n * with `variable` slanted every plain identifier in the file, which is the most\n * common token class there is. Give an italic scope its own entry.\n */\nconst SCOPE_COLORS: ReadonlyArray<{\n readonly scopes: string[];\n readonly token: string;\n readonly italic?: boolean;\n}> = [\n {\n scopes: [\"comment\", \"punctuation.definition.comment\"],\n token: \"--code-comment\",\n italic: true,\n },\n { scopes: [\"string\", \"string.quoted\", \"punctuation.definition.string\"], token: \"--code-string\" },\n {\n scopes: [\"constant\", \"constant.language\", \"constant.character\", \"support.constant\"],\n token: \"--code-constant\",\n },\n { scopes: [\"constant.numeric\", \"keyword.other.unit\"], token: \"--code-number\" },\n {\n scopes: [\"keyword\", \"keyword.control\", \"keyword.operator\", \"storage\", \"storage.type\"],\n token: \"--code-keyword\",\n },\n {\n scopes: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n token: \"--code-function\",\n },\n {\n scopes: [\"entity.name.type\", \"entity.name.class\", \"support.type\", \"support.class\"],\n token: \"--code-type\",\n },\n { scopes: [\"entity.name.tag\", \"punctuation.definition.tag\"], token: \"--code-tag\" },\n { scopes: [\"variable\", \"support.variable\", \"meta.object-literal.key\"], token: \"--code-property\" },\n // Same colour as a property, but the one other scope that leans. Its own rule,\n // for the reason in the docblock above.\n { scopes: [\"entity.other.attribute-name\"], token: \"--code-property\", italic: true },\n];\n\n/**\n * The theme, built once at module load. Shiki treats a theme object as opaque\n * data, so one instance is safely shared by every highlighter.\n */\nexport const codeViewerTheme: ThemeRegistrationRaw = {\n name: \"brand-ui-viewer\",\n // Shiki uses `type` only to pick its own light/dark defaults, which this theme\n // overrides wholesale. It is `css` — neither, resolved at paint time.\n type: \"dark\",\n colors: {\n // The renderer owns the surface; the theme must not paint over it.\n \"editor.background\": \"transparent\",\n \"editor.foreground\": \"var(--code-foreground)\",\n },\n fg: \"var(--code-foreground)\",\n bg: \"transparent\",\n settings: [\n { settings: { foreground: \"var(--code-foreground)\" } },\n ...SCOPE_COLORS.map(({ scopes, token, italic }) => ({\n scope: scopes,\n settings: {\n foreground: `var(${token})`,\n ...(italic ? { fontStyle: \"italic\" } : undefined),\n },\n })),\n ],\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/csv/csv-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CSV / TSV adapter — a real parser and a real table.\n *\n * Two things this fixes at once:\n *\n * - **Correctness.** The repo's previous CSV \"parser\" was `line.split(\",\")`\n * (`packages/ai/src/asset-preview.tsx:69`), which mangles every quoted comma —\n * i.e. every address, every price list, every free-text column. Papa Parse\n * handles quoting, escapes, embedded newlines and delimiter detection.\n * - **Theming.** anyview's CSV adapter emits an HTML string with `#ddd` and\n * `#f5f5f5` baked in (`CsvAdapter.ts:184`). This one emits ROWS and hands them\n * to `@elabs-ai/components-ui`'s `Table`, so the grid is tokened and\n * re-themes with everything else.\n *\n * `papaparse` is an OPTIONAL peer, reached only through the dynamic import\n * below. A consumer who never opens a spreadsheet never installs it, and one who\n * forgot gets a \"install papaparse\" message rather than a resolution stack trace\n * (`isModuleNotFound` in `core/errors.ts`).\n */\n\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { useMemo } from \"react\";\n\nimport { gridToText, type GridRef } from \"../../components/grid-text\";\nimport { SheetTable } from \"../../components/sheet-table\";\n\nimport { ViewerError, toViewerError } from \"../../core/errors\";\nimport { toMarkRanges } from \"../../core/highlight-marks\";\nimport type { TextIndex } from \"../../core/text-index\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { csvManifest } from \"./csv-manifest\";\n\n/**\n * Rows rendered before truncation. A preview pane is for looking, not for\n * analysis — past this, reach for `@elabs-ai/components-data`'s\n * `DataTable`, which virtualizes.\n */\nexport const CSV_ROW_LIMIT = 5_000;\n\nexport interface CsvDocument extends AdapterDocument {\n kind: \"csv\";\n /** Header cells. Empty when the file had no usable first row. */\n columns: string[];\n /** Body rows, already capped at {@link CSV_ROW_LIMIT}. */\n rows: string[][];\n /** Total body rows in the file, when more than what is shown. */\n totalRows?: number;\n /** Which row each stretch of `text` came from. */\n textIndex?: TextIndex<GridRef>;\n}\n\nclass CsvAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<CsvDocument> {\n let text: string;\n try {\n text = await source.text(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Dynamic: this is the ONLY edge to the optional peer. A static import here\n // would break every consumer that did not install it (heavy-deps:check).\n const Papa = await import(\"papaparse\");\n const parsed = Papa.parse<string[]>(text, {\n skipEmptyLines: \"greedy\",\n // Let Papa detect `,` vs `\\t` vs `;` rather than trusting the extension —\n // a .csv exported by a European locale is very often semicolon-delimited.\n delimiter: \"\",\n });\n\n // Papa reports recoverable problems as errors while still returning data;\n // only an empty result with errors is genuinely unreadable.\n if (parsed.data.length === 0 && parsed.errors.length > 0) {\n throw new ViewerError(\n \"parse-failed\",\n parsed.errors[0]?.message ?? \"Could not parse the file.\",\n {\n fileName: source.name,\n },\n );\n }\n\n const [header = [], ...body] = parsed.data;\n const rows = body.length <= CSV_ROW_LIMIT ? body : body.slice(0, CSV_ROW_LIMIT);\n // `text` is the PARSED grid, not the raw file (changed in this release).\n // A citation's offsets have to address what the reader can see: the raw\n // bytes carry quoting, escapes and a delimiter that may not be a comma, so\n // an offset into them lands nowhere in particular in the rendered table.\n const textIndex = gridToText([{ columns: header, rows }]);\n return {\n kind: \"csv\",\n columns: header,\n rows,\n ...(body.length > CSV_ROW_LIMIT ? { totalRows: body.length } : {}),\n text: textIndex.text,\n textIndex,\n };\n }\n}\n\nfunction CsvRenderer({\n document: doc,\n className,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const csv = doc as CsvDocument;\n const marks = useMemo(\n () => toMarkRanges(highlights, csv.text?.length ?? 0),\n [highlights, csv.text],\n );\n // One sheet, so a row's start is simply the span whose ref names that row.\n const rowStart = useMemo(() => {\n const starts = new Map<number, number>();\n for (const span of csv.textIndex?.spans ?? []) starts.set(span.ref.row, span.start);\n return (row: number) => starts.get(row);\n }, [csv.textIndex]);\n\n return (\n <SheetTable\n columns={csv.columns}\n rows={csv.rows}\n totalRows={csv.totalRows}\n className={className}\n marks={marks}\n rowStart={rowStart}\n activeHighlightId={activeHighlightId}\n />\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: csvManifest,\n create: () => new CsvAdapter(),\n Renderer: CsvRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,eAAe;AAwGpB;AAlFG,IAAM,gBAAgB;AAc7B,IAAM,aAAN,MAAwC;AAAA,EACtC,MAAM,KAAK,QAA4B,SAAmD;AACxF,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,OAAO,KAAK,QAAQ,MAAM;AAAA,IACzC,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,eAAe,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACrE;AAIA,UAAM,OAAO,MAAM,OAAO,WAAW;AACrC,UAAM,SAAS,KAAK,MAAgB,MAAM;AAAA,MACxC,gBAAgB;AAAA;AAAA;AAAA,MAGhB,WAAW;AAAA,IACb,CAAC;AAID,QAAI,OAAO,KAAK,WAAW,KAAK,OAAO,OAAO,SAAS,GAAG;AACxD,YAAM,IAAI;AAAA,QACR;AAAA,QACA,OAAO,OAAO,CAAC,GAAG,WAAW;AAAA,QAC7B;AAAA,UACE,UAAU,OAAO;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,CAAC,SAAS,CAAC,GAAG,GAAG,IAAI,IAAI,OAAO;AACtC,UAAM,OAAO,KAAK,UAAU,gBAAgB,OAAO,KAAK,MAAM,GAAG,aAAa;AAK9E,UAAM,YAAY,WAAW,CAAC,EAAE,SAAS,QAAQ,KAAK,CAAC,CAAC;AACxD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,MACT;AAAA,MACA,GAAI,KAAK,SAAS,gBAAgB,EAAE,WAAW,KAAK,OAAO,IAAI,CAAC;AAAA,MAChE,MAAM,UAAU;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,YAAY;AAAA,EACnB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,MAAM;AACZ,QAAM,QAAQ;AAAA,IACZ,MAAM,aAAa,YAAY,IAAI,MAAM,UAAU,CAAC;AAAA,IACpD,CAAC,YAAY,IAAI,IAAI;AAAA,EACvB;AAEA,QAAM,WAAW,QAAQ,MAAM;AAC7B,UAAM,SAAS,oBAAI,IAAoB;AACvC,eAAW,QAAQ,IAAI,WAAW,SAAS,CAAC,EAAG,QAAO,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK;AAClF,WAAO,CAAC,QAAgB,OAAO,IAAI,GAAG;AAAA,EACxC,GAAG,CAAC,IAAI,SAAS,CAAC;AAElB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAS,IAAI;AAAA,MACb,MAAM,IAAI;AAAA,MACV,WAAW,IAAI;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,QAAQ,MAAM,IAAI,WAAW;AAAA,EAC7B,UAAU;AACZ;AAEA,IAAO,sBAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/docx/docx-adapter.tsx","../src/adapters/docx/docx-model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Word adapter — a `.docx` becomes blocks, and blocks become brand-ui prose.\n *\n * mammoth does the hard part (unzipping OOXML, resolving styles, numbering,\n * images); `docx-model.ts` then parses its HTML into a model and discards the\n * markup, so nothing here writes `innerHTML`. That single decision is what makes\n * a Word document themeable, Trusted-Types-safe and free of a sanitizer\n * dependency — the reasoning lives in `docx-model.ts`.\n *\n * What a reader gets is the document's STRUCTURE in this system's typography:\n * real headings, real lists, a real `Table`, images with their own alt text. It\n * is not a pixel reproduction of Word's page layout — no page breaks, no columns,\n * no margins — and it does not pretend to be. For a byte-faithful rendering the\n * honest answer is to download the file, which the toolbar already offers.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport {\n cn,\n ProseHeading,\n ProseLink,\n ProseList,\n ProseListItem,\n ProseText,\n StatePanel,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n useLocale,\n} from \"@elabs-ai/components-ui\";\n\nimport { useMemo, useRef } from \"react\";\n\nimport { MarkedText } from \"../../components/marked-text\";\nimport { toViewerError } from \"../../core/errors\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport type { TextIndex, TextSpan } from \"../../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport {\n blocksToTextWithMap,\n DOCX_CELL_SEPARATOR,\n DOCX_HEAD_ROW,\n DOCX_LIST_BULLET,\n htmlToBlocks,\n type DocxBlock,\n type DocxRef,\n type DocxRun,\n} from \"./docx-model\";\nimport { docxManifest } from \"./docx-manifest\";\n\nexport interface DocxDocument extends AdapterDocument {\n kind: \"docx\";\n blocks: DocxBlock[];\n /** Non-fatal conversion notes mammoth reported (an unsupported style, a dropped field). */\n warnings: string[];\n /** Which block, item or row each stretch of `text` came from. */\n textIndex?: TextIndex<DocxRef>;\n}\n\n/**\n * Parse markup with the DOM the runtime already has.\n *\n * `DOMParser` is the browser's own parser and jsdom's in tests — it does NOT\n * execute anything it parses, which is why building the model from it is safe\n * even though the input is third-party markup.\n */\nfunction parseMarkup(markup: string): Document {\n return new DOMParser().parseFromString(markup, \"text/html\");\n}\n\nclass DocxAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<DocxDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const mammoth = await import(\"mammoth\");\n\n try {\n const result = await mammoth.convertToHtml({ arrayBuffer: buffer });\n const blocks = htmlToBlocks(result.value, parseMarkup);\n const textIndex = blocksToTextWithMap(blocks);\n return {\n kind: \"docx\",\n blocks,\n // mammoth's messages are about the CONVERSION, not about the file being\n // broken — a document that lost one custom style still reads fine, so\n // they are carried as notes rather than raised as a failure.\n warnings: result.messages.map((message) => message.message),\n text: textIndex.text,\n textIndex,\n };\n } catch (error) {\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\nfunction Runs({ runs, marks, start }: { runs: DocxRun[]; marks: MarkRanges; start?: number }) {\n let offset = start;\n return (\n <>\n {runs.map((run, index) => {\n const from = offset;\n if (offset !== undefined) offset += run.text.length;\n // Runs are joined with nothing in the projection, so a run's offset is\n // simply the sum of the ones before it — the same accumulation the code\n // adapter does over syntax tokens.\n const content = <MarkedText text={run.text} marks={marks} start={from} />;\n const bolded = run.bold ? <strong>{content}</strong> : content;\n const styled = run.italic ? <em>{bolded}</em> : bolded;\n if (run.href) {\n return (\n <ProseLink key={index} href={run.href}>\n {styled}\n </ProseLink>\n );\n }\n return <span key={index}>{styled}</span>;\n })}\n </>\n );\n}\n\n/** Shared empty list, so a block with no spans does not remount on every render. */\nconst EMPTY_SPANS: readonly TextSpan<DocxRef>[] = [];\n\n/** The projection offset of one row's `index`-th cell, given the row's own start. */\nfunction cellStart(row: readonly string[], index: number, rowStart?: number): number | undefined {\n if (rowStart === undefined) return undefined;\n let offset = rowStart;\n for (let i = 0; i < index; i += 1) {\n offset += (row[i]?.length ?? 0) + DOCX_CELL_SEPARATOR.length;\n }\n return offset;\n}\n\nfunction Block({\n block,\n baseHeadingLevel,\n marks,\n spans,\n}: {\n block: DocxBlock;\n baseHeadingLevel: number;\n marks: MarkRanges;\n /** The projection spans belonging to THIS block, in document order. */\n spans: readonly TextSpan<DocxRef>[];\n}) {\n const startOf = (match: (ref: DocxRef) => boolean) =>\n spans.find((span) => match(span.ref))?.start;\n\n if (block.type === \"heading\") {\n return (\n // Offset, not absolute: Word's \"Heading 1\" is the top of THAT document,\n // not of the page showing it. Never past h6 — an h7 is not an element.\n <ProseHeading\n level={Math.min(6, Math.max(1, block.level + baseHeadingLevel - 1)) as ProseHeadingLevel}\n >\n <Runs runs={block.runs} marks={marks} start={startOf(() => true)} />\n </ProseHeading>\n );\n }\n if (block.type === \"paragraph\") {\n return (\n <ProseText className=\"whitespace-pre-wrap\">\n <Runs runs={block.runs} marks={marks} start={startOf(() => true)} />\n </ProseText>\n );\n }\n if (block.type === \"list\") {\n return (\n <ProseList ordered={block.ordered}>\n {block.items.map((item, index) => {\n // The bullet is in the projection but not in the DOM — the list\n // element draws it — so an item's own text starts after it.\n const start = startOf((ref) => ref.item === index);\n return (\n <ProseListItem key={index}>\n <Runs\n runs={item}\n marks={marks}\n start={start === undefined ? undefined : start + DOCX_LIST_BULLET.length}\n />\n </ProseListItem>\n );\n })}\n </ProseList>\n );\n }\n if (block.type === \"image\") {\n return (\n <img\n src={block.src}\n // The document's own alt text when the author wrote one. An image with\n // none is decoration as far as the reader can tell, and a filename would\n // be noise, not a description.\n alt={block.alt ?? \"\"}\n {...(block.alt ? {} : { \"aria-hidden\": true })}\n className=\"my-2 block h-auto max-w-full rounded-md\"\n />\n );\n }\n const headStart = startOf((ref) => ref.row === DOCX_HEAD_ROW);\n return (\n <Table className=\"my-2\">\n {block.head && (\n <TableHeader>\n <TableRow>\n {block.head.map((cell, index) => (\n <TableHead key={index} scope=\"col\">\n <MarkedText\n text={cell}\n marks={marks}\n start={cellStart(block.head ?? [], index, headStart)}\n />\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n )}\n <TableBody>\n {block.rows.map((row, rowIndex) => {\n const rowStart = startOf((ref) => ref.row === rowIndex);\n return (\n <TableRow key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <TableCell key={cellIndex} className=\"whitespace-pre-wrap align-top\">\n <MarkedText\n text={cell}\n marks={marks}\n start={cellStart(row, cellIndex, rowStart)}\n />\n </TableCell>\n ))}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n );\n}\n\nfunction DocxRenderer({\n document: doc,\n className,\n baseHeadingLevel = 2,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const docx = doc as DocxDocument;\n const { t } = useLocale();\n const container = useRef<HTMLElement>(null);\n\n const marks = useMemo(\n () => toMarkRanges(highlights, docx.text?.length ?? 0),\n [highlights, docx.text],\n );\n // Grouped once per document rather than searched per block: a long report is\n // thousands of blocks, and a linear scan inside the render loop would make it\n // quadratic on every keystroke of a find-as-you-type.\n const spansByBlock = useMemo(() => {\n const map = new Map<number, TextSpan<DocxRef>[]>();\n for (const span of docx.textIndex?.spans ?? []) {\n const list = map.get(span.ref.block);\n if (list) list.push(span);\n else map.set(span.ref.block, [span]);\n }\n return map;\n }, [docx.textIndex]);\n\n useScrollActiveHighlightIntoView(container, activeHighlightId);\n\n if (docx.blocks.length === 0) {\n return (\n <div className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}>\n <StatePanel kind=\"empty\" title={t(\"viewer.docx.empty\")} />\n </div>\n );\n }\n\n return (\n // `max-w-prose` because this is genuine multi-sentence prose in a pane that\n // can be very wide — the one case styling-and-tokens.md says to cap the\n // measure. Centred so the column does not hug one edge. No `overflow-auto`:\n // `FileViewerContent` is the scroll boundary, and a second one clips the\n // last paragraph above the outer pane's padding.\n <article ref={container} className={cn(\"mx-auto max-w-prose space-y-2\", className)}>\n {docx.blocks.map((block, index) => (\n <Block\n key={index}\n block={block}\n baseHeadingLevel={baseHeadingLevel}\n marks={marks}\n spans={spansByBlock.get(index) ?? EMPTY_SPANS}\n />\n ))}\n </article>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: docxManifest,\n create: () => new DocxAdapter(),\n Renderer: DocxRenderer,\n};\n\nexport default adapterModule;\n","/**\n * Word markup → a block model.\n *\n * ## Why this exists at all\n *\n * mammoth converts a `.docx` into an HTML **string**. Rendering that string\n * would mean `dangerouslySetInnerHTML`, which is three separate problems: it\n * puts third-party markup past the token layer, it needs a sanitizer to be safe\n * at all, and it makes the surface unusable under a Trusted-Types policy (a raw\n * `innerHTML` write throws during commit and takes the React root down with it —\n * see `docs/CSP-AND-NETWORK.md`).\n *\n * So the HTML is parsed and thrown away, and what survives is a small model of\n * blocks and inline runs that React renders as real elements. **The parse IS the\n * allowlist**: a tag this file does not name contributes nothing but its text,\n * so there is no denylist to keep up to date, no `<script>` to strip, and no\n * sanitizer dependency. It is also why the viewer adds no CSP sink.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../../core/text-index\";\n\n/** A styled span of text inside a block. */\nexport interface DocxRun {\n text: string;\n bold?: boolean;\n italic?: boolean;\n /** Present only for a link whose scheme survived {@link safeHref}. */\n href?: string;\n}\n\nexport type DocxBlock =\n | { type: \"heading\"; level: 1 | 2 | 3 | 4 | 5 | 6; runs: DocxRun[] }\n | { type: \"paragraph\"; runs: DocxRun[] }\n | { type: \"list\"; ordered: boolean; items: DocxRun[][] }\n | { type: \"table\"; head?: string[]; rows: string[][] }\n | { type: \"image\"; src: string; alt?: string };\n\n/** Schemes a link may keep. Anything else (notably `javascript:`) becomes plain text. */\nconst SAFE_SCHEMES = [\"http:\", \"https:\", \"mailto:\", \"tel:\"];\n\n/**\n * The href, or `undefined` when it is not a scheme we will hand to the browser.\n * Relative hrefs are dropped too — a Word document's internal bookmarks point at\n * anchors that do not exist on the host page.\n */\nexport function safeHref(value: string | null): string | undefined {\n if (!value) return undefined;\n try {\n const url = new URL(value, \"https://viewer.invalid\");\n return SAFE_SCHEMES.includes(url.protocol) && !value.startsWith(\"#\") ? url.href : undefined;\n } catch {\n return undefined;\n }\n}\n\ninterface RunStyle {\n bold?: boolean;\n italic?: boolean;\n href?: string;\n}\n\n/** Collect the inline runs under a node, carrying style down the tree. */\nfunction collectRuns(node: Node, style: RunStyle = {}, out: DocxRun[] = []): DocxRun[] {\n for (const child of Array.from(node.childNodes)) {\n if (child.nodeType === 3 /* TEXT_NODE */) {\n const text = child.nodeValue ?? \"\";\n if (text) out.push({ text, ...style });\n continue;\n }\n if (child.nodeType !== 1 /* ELEMENT_NODE */) continue;\n\n const element = child as Element;\n const tag = element.tagName.toLowerCase();\n if (tag === \"br\") {\n out.push({ text: \"\\n\", ...style });\n continue;\n }\n const next: RunStyle = { ...style };\n if (tag === \"strong\" || tag === \"b\") next.bold = true;\n if (tag === \"em\" || tag === \"i\") next.italic = true;\n if (tag === \"a\") {\n const href = safeHref(element.getAttribute(\"href\"));\n if (href) next.href = href;\n }\n collectRuns(element, next, out);\n }\n return out;\n}\n\n/** Merge adjacent runs that carry identical styling — fewer, longer spans. */\nfunction compact(runs: DocxRun[]): DocxRun[] {\n const merged: DocxRun[] = [];\n for (const run of runs) {\n const last = merged[merged.length - 1];\n if (last && last.bold === run.bold && last.italic === run.italic && last.href === run.href) {\n last.text += run.text;\n } else {\n merged.push({ ...run });\n }\n }\n return merged.filter((run) => run.text.trim() !== \"\" || run.text.includes(\"\\n\"));\n}\n\nfunction textOf(node: Node): string {\n return compact(collectRuns(node))\n .map((run) => run.text)\n .join(\"\")\n .trim();\n}\n\nfunction listItems(element: Element): DocxRun[][] {\n const items: DocxRun[][] = [];\n for (const child of Array.from(element.children)) {\n if (child.tagName.toLowerCase() !== \"li\") continue;\n // A nested list inside an `<li>` is flattened into its own items rather than\n // being rendered as a sub-list: the depth is not worth a recursive component\n // in a preview, and losing it never loses the WORDS.\n const runs = compact(collectRuns(child));\n if (runs.length > 0) items.push(runs);\n for (const nested of Array.from(child.children)) {\n const tag = nested.tagName.toLowerCase();\n if (tag === \"ul\" || tag === \"ol\") items.push(...listItems(nested));\n }\n }\n return items;\n}\n\nfunction tableBlock(element: Element): DocxBlock | undefined {\n const rows = Array.from(element.querySelectorAll(\"tr\"));\n if (rows.length === 0) return undefined;\n\n const cellsOf = (row: Element) =>\n Array.from(row.children)\n .filter((cell) => [\"td\", \"th\"].includes(cell.tagName.toLowerCase()))\n .map(textOf);\n\n const [firstRow, ...restRows] = rows;\n if (!firstRow) return undefined;\n\n // Word only emits `<th>` when the author marked a header row. Without it the\n // first row is ordinary data, and promoting it would invent a header.\n const isHeader = firstRow.querySelector(\"th\") !== null;\n return isHeader\n ? { type: \"table\", head: cellsOf(firstRow), rows: restRows.map(cellsOf) }\n : { type: \"table\", rows: rows.map(cellsOf) };\n}\n\nconst HEADING_LEVELS: Record<string, 1 | 2 | 3 | 4 | 5 | 6> = {\n h1: 1,\n h2: 2,\n h3: 3,\n h4: 4,\n h5: 5,\n h6: 6,\n};\n\n/**\n * Parse mammoth's HTML into blocks.\n *\n * Takes a `DOMParser` factory so the caller decides where the parser comes from\n * — the browser's own in an app, jsdom's in a test — instead of this module\n * reaching for a global that may not exist on a server.\n */\nexport function htmlToBlocks(html: string, parse: (markup: string) => Document): DocxBlock[] {\n const document = parse(`<body>${html}</body>`);\n const blocks: DocxBlock[] = [];\n\n const walk = (parent: Element) => {\n for (const element of Array.from(parent.children)) {\n const tag = element.tagName.toLowerCase();\n const level = HEADING_LEVELS[tag];\n\n if (level) {\n const runs = compact(collectRuns(element));\n if (runs.length > 0) blocks.push({ type: \"heading\", level, runs });\n continue;\n }\n if (tag === \"p\") {\n // A `<p>` holding only an image is the image, not an empty paragraph.\n const image = element.querySelector(\"img\");\n const runs = compact(collectRuns(element));\n if (image && runs.length === 0) {\n blocks.push(imageBlock(image));\n continue;\n }\n if (runs.length > 0) blocks.push({ type: \"paragraph\", runs });\n continue;\n }\n if (tag === \"ul\" || tag === \"ol\") {\n const items = listItems(element);\n if (items.length > 0) blocks.push({ type: \"list\", ordered: tag === \"ol\", items });\n continue;\n }\n if (tag === \"table\") {\n const table = tableBlock(element);\n if (table) blocks.push(table);\n continue;\n }\n if (tag === \"img\") {\n blocks.push(imageBlock(element));\n continue;\n }\n // Anything else — a `div`, a `section`, a tag mammoth grew in a later\n // version — contributes its CHILDREN, never itself. That is what makes\n // this an allowlist that cannot silently let markup through.\n walk(element);\n }\n };\n\n walk(document.body);\n return blocks;\n}\n\nfunction imageBlock(element: Element): DocxBlock {\n const alt = element.getAttribute(\"alt\")?.trim();\n return {\n type: \"image\",\n // mammoth inlines images as `data:` URIs by default, so there is no remote\n // origin here — and no `src` at all is better than a URL we did not vet.\n src: element.getAttribute(\"src\") ?? \"\",\n ...(alt ? { alt } : {}),\n };\n}\n\n/** Written before a list item's own text in the projection. */\nexport const DOCX_LIST_BULLET = \"• \";\n\n/** Written between two cells of the same row. */\nexport const DOCX_CELL_SEPARATOR = \"\\t\";\n\n/** The header row's `row`, so one field can name every row of a table. */\nexport const DOCX_HEAD_ROW = -1;\n\n/**\n * Where a stretch of the projection came from in the block model.\n *\n * Deliberately one shape with optional fields rather than a union: the renderer\n * looks a span up by \"this block, that item\" in three places, and a union would\n * make each of those a type narrowing exercise for no gain.\n */\nexport interface DocxRef {\n /** Index into the `blocks` array. */\n block: number;\n /** Which item, for a `list` block. */\n item?: number;\n /** Which row, for a `table` block. {@link DOCX_HEAD_ROW} is the header. */\n row?: number;\n}\n\n/**\n * Plain-text projection of a parsed document, plus the map back into the blocks.\n *\n * Rows rather than cells are the finest ref: one builder has one separator, and\n * a row's cells are joined by a tab while the rows themselves are joined by a\n * newline. The renderer walks a row's cells the same way it walks a paragraph's\n * runs — accumulating lengths — so the granularity of the MARK is still the\n * character, even though the granularity of the REF is the row.\n */\nexport function blocksToTextWithMap(blocks: DocxBlock[]): TextIndex<DocxRef> {\n const builder = createTextIndexBuilder<DocxRef>();\n blocks.forEach((block, index) => {\n if (block.type === \"heading\" || block.type === \"paragraph\") {\n builder.push(block.runs.map((run) => run.text).join(\"\"), { block: index });\n return;\n }\n if (block.type === \"list\") {\n block.items.forEach((item, item_) => {\n builder.push(`${DOCX_LIST_BULLET}${item.map((run) => run.text).join(\"\")}`, {\n block: index,\n item: item_,\n });\n });\n return;\n }\n if (block.type === \"table\") {\n if (block.head) {\n builder.push(block.head.join(DOCX_CELL_SEPARATOR), { block: index, row: DOCX_HEAD_ROW });\n }\n block.rows.forEach((row, rowIndex) => {\n builder.push(row.join(DOCX_CELL_SEPARATOR), { block: index, row: rowIndex });\n });\n }\n });\n return builder.build();\n}\n\n/**\n * Plain-text projection of a parsed document — powers search, copy and the raw\n * view. A thin wrapper so the projection has exactly one definition: it is the\n * index's own text, never a second assembly that could drift from it.\n */\nexport function blocksToText(blocks: DocxBlock[]): string {\n return blocksToTextWithMap(blocks).text;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,SAAS,cAAc;;;ACEhC,IAAM,eAAe,CAAC,SAAS,UAAU,WAAW,MAAM;AAOnD,SAAS,SAAS,OAA0C;AACjE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO,wBAAwB;AACnD,WAAO,aAAa,SAAS,IAAI,QAAQ,KAAK,CAAC,MAAM,WAAW,GAAG,IAAI,IAAI,OAAO;AAAA,EACpF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,SAAS,YAAY,MAAY,QAAkB,CAAC,GAAG,MAAiB,CAAC,GAAc;AACrF,aAAW,SAAS,MAAM,KAAK,KAAK,UAAU,GAAG;AAC/C,QAAI,MAAM,aAAa,GAAmB;AACxC,YAAM,OAAO,MAAM,aAAa;AAChC,UAAI,KAAM,KAAI,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;AACrC;AAAA,IACF;AACA,QAAI,MAAM,aAAa,EAAsB;AAE7C,UAAM,UAAU;AAChB,UAAM,MAAM,QAAQ,QAAQ,YAAY;AACxC,QAAI,QAAQ,MAAM;AAChB,UAAI,KAAK,EAAE,MAAM,MAAM,GAAG,MAAM,CAAC;AACjC;AAAA,IACF;AACA,UAAM,OAAiB,EAAE,GAAG,MAAM;AAClC,QAAI,QAAQ,YAAY,QAAQ,IAAK,MAAK,OAAO;AACjD,QAAI,QAAQ,QAAQ,QAAQ,IAAK,MAAK,SAAS;AAC/C,QAAI,QAAQ,KAAK;AACf,YAAM,OAAO,SAAS,QAAQ,aAAa,MAAM,CAAC;AAClD,UAAI,KAAM,MAAK,OAAO;AAAA,IACxB;AACA,gBAAY,SAAS,MAAM,GAAG;AAAA,EAChC;AACA,SAAO;AACT;AAGA,SAAS,QAAQ,MAA4B;AAC3C,QAAM,SAAoB,CAAC;AAC3B,aAAW,OAAO,MAAM;AACtB,UAAM,OAAO,OAAO,OAAO,SAAS,CAAC;AACrC,QAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,WAAW,IAAI,UAAU,KAAK,SAAS,IAAI,MAAM;AAC1F,WAAK,QAAQ,IAAI;AAAA,IACnB,OAAO;AACL,aAAO,KAAK,EAAE,GAAG,IAAI,CAAC;AAAA,IACxB;AAAA,EACF;AACA,SAAO,OAAO,OAAO,CAAC,QAAQ,IAAI,KAAK,KAAK,MAAM,MAAM,IAAI,KAAK,SAAS,IAAI,CAAC;AACjF;AAEA,SAAS,OAAO,MAAoB;AAClC,SAAO,QAAQ,YAAY,IAAI,CAAC,EAC7B,IAAI,CAAC,QAAQ,IAAI,IAAI,EACrB,KAAK,EAAE,EACP,KAAK;AACV;AAEA,SAAS,UAAU,SAA+B;AAChD,QAAM,QAAqB,CAAC;AAC5B,aAAW,SAAS,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAChD,QAAI,MAAM,QAAQ,YAAY,MAAM,KAAM;AAI1C,UAAM,OAAO,QAAQ,YAAY,KAAK,CAAC;AACvC,QAAI,KAAK,SAAS,EAAG,OAAM,KAAK,IAAI;AACpC,eAAW,UAAU,MAAM,KAAK,MAAM,QAAQ,GAAG;AAC/C,YAAM,MAAM,OAAO,QAAQ,YAAY;AACvC,UAAI,QAAQ,QAAQ,QAAQ,KAAM,OAAM,KAAK,GAAG,UAAU,MAAM,CAAC;AAAA,IACnE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,SAAyC;AAC3D,QAAM,OAAO,MAAM,KAAK,QAAQ,iBAAiB,IAAI,CAAC;AACtD,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,UAAU,CAAC,QACf,MAAM,KAAK,IAAI,QAAQ,EACpB,OAAO,CAAC,SAAS,CAAC,MAAM,IAAI,EAAE,SAAS,KAAK,QAAQ,YAAY,CAAC,CAAC,EAClE,IAAI,MAAM;AAEf,QAAM,CAAC,UAAU,GAAG,QAAQ,IAAI;AAChC,MAAI,CAAC,SAAU,QAAO;AAItB,QAAM,WAAW,SAAS,cAAc,IAAI,MAAM;AAClD,SAAO,WACH,EAAE,MAAM,SAAS,MAAM,QAAQ,QAAQ,GAAG,MAAM,SAAS,IAAI,OAAO,EAAE,IACtE,EAAE,MAAM,SAAS,MAAM,KAAK,IAAI,OAAO,EAAE;AAC/C;AAEA,IAAM,iBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AASO,SAAS,aAAa,MAAc,OAAkD;AAC3F,QAAM,WAAW,MAAM,SAAS,IAAI,SAAS;AAC7C,QAAM,SAAsB,CAAC;AAE7B,QAAM,OAAO,CAAC,WAAoB;AAChC,eAAW,WAAW,MAAM,KAAK,OAAO,QAAQ,GAAG;AACjD,YAAM,MAAM,QAAQ,QAAQ,YAAY;AACxC,YAAM,QAAQ,eAAe,GAAG;AAEhC,UAAI,OAAO;AACT,cAAM,OAAO,QAAQ,YAAY,OAAO,CAAC;AACzC,YAAI,KAAK,SAAS,EAAG,QAAO,KAAK,EAAE,MAAM,WAAW,OAAO,KAAK,CAAC;AACjE;AAAA,MACF;AACA,UAAI,QAAQ,KAAK;AAEf,cAAM,QAAQ,QAAQ,cAAc,KAAK;AACzC,cAAM,OAAO,QAAQ,YAAY,OAAO,CAAC;AACzC,YAAI,SAAS,KAAK,WAAW,GAAG;AAC9B,iBAAO,KAAK,WAAW,KAAK,CAAC;AAC7B;AAAA,QACF;AACA,YAAI,KAAK,SAAS,EAAG,QAAO,KAAK,EAAE,MAAM,aAAa,KAAK,CAAC;AAC5D;AAAA,MACF;AACA,UAAI,QAAQ,QAAQ,QAAQ,MAAM;AAChC,cAAM,QAAQ,UAAU,OAAO;AAC/B,YAAI,MAAM,SAAS,EAAG,QAAO,KAAK,EAAE,MAAM,QAAQ,SAAS,QAAQ,MAAM,MAAM,CAAC;AAChF;AAAA,MACF;AACA,UAAI,QAAQ,SAAS;AACnB,cAAM,QAAQ,WAAW,OAAO;AAChC,YAAI,MAAO,QAAO,KAAK,KAAK;AAC5B;AAAA,MACF;AACA,UAAI,QAAQ,OAAO;AACjB,eAAO,KAAK,WAAW,OAAO,CAAC;AAC/B;AAAA,MACF;AAIA,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAEA,OAAK,SAAS,IAAI;AAClB,SAAO;AACT;AAEA,SAAS,WAAW,SAA6B;AAC/C,QAAM,MAAM,QAAQ,aAAa,KAAK,GAAG,KAAK;AAC9C,SAAO;AAAA,IACL,MAAM;AAAA;AAAA;AAAA,IAGN,KAAK,QAAQ,aAAa,KAAK,KAAK;AAAA,IACpC,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,EACvB;AACF;AAGO,IAAM,mBAAmB;AAGzB,IAAM,sBAAsB;AAG5B,IAAM,gBAAgB;AA2BtB,SAAS,oBAAoB,QAAyC;AAC3E,QAAM,UAAU,uBAAgC;AAChD,SAAO,QAAQ,CAAC,OAAO,UAAU;AAC/B,QAAI,MAAM,SAAS,aAAa,MAAM,SAAS,aAAa;AAC1D,cAAQ,KAAK,MAAM,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,MAAM,CAAC;AACzE;AAAA,IACF;AACA,QAAI,MAAM,SAAS,QAAQ;AACzB,YAAM,MAAM,QAAQ,CAAC,MAAM,UAAU;AACnC,gBAAQ,KAAK,GAAG,gBAAgB,GAAG,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC,IAAI;AAAA,UACzE,OAAO;AAAA,UACP,MAAM;AAAA,QACR,CAAC;AAAA,MACH,CAAC;AACD;AAAA,IACF;AACA,QAAI,MAAM,SAAS,SAAS;AAC1B,UAAI,MAAM,MAAM;AACd,gBAAQ,KAAK,MAAM,KAAK,KAAK,mBAAmB,GAAG,EAAE,OAAO,OAAO,KAAK,cAAc,CAAC;AAAA,MACzF;AACA,YAAM,KAAK,QAAQ,CAAC,KAAK,aAAa;AACpC,gBAAQ,KAAK,IAAI,KAAK,mBAAmB,GAAG,EAAE,OAAO,OAAO,KAAK,SAAS,CAAC;AAAA,MAC7E,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACD,SAAO,QAAQ,MAAM;AACvB;;;ADnKI,mBAOoB,KAiGpB,YAxGA;AA3CJ,SAAS,YAAY,QAA0B;AAC7C,SAAO,IAAI,UAAU,EAAE,gBAAgB,QAAQ,WAAW;AAC5D;AAEA,IAAM,cAAN,MAAyC;AAAA,EACvC,MAAM,KAAK,QAA4B,SAAoD;AACzF,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,OAAO,MAAM,QAAQ,MAAM;AAAA,IAC5C,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,eAAe,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACrE;AAGA,UAAM,UAAU,MAAM,OAAO,SAAS;AAEtC,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,cAAc,EAAE,aAAa,OAAO,CAAC;AAClE,YAAM,SAAS,aAAa,OAAO,OAAO,WAAW;AACrD,YAAM,YAAY,oBAAoB,MAAM;AAC5C,aAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA;AAAA;AAAA;AAAA,QAIA,UAAU,OAAO,SAAS,IAAI,CAAC,YAAY,QAAQ,OAAO;AAAA,QAC1D,MAAM,UAAU;AAAA,QAChB;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,gBAAgB,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACtE;AAAA,EACF;AACF;AAMA,SAAS,KAAK,EAAE,MAAM,OAAO,MAAM,GAA2D;AAC5F,MAAI,SAAS;AACb,SACE,gCACG,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,OAAO;AACb,QAAI,WAAW,OAAW,WAAU,IAAI,KAAK;AAI7C,UAAM,UAAU,oBAAC,cAAW,MAAM,IAAI,MAAM,OAAc,OAAO,MAAM;AACvE,UAAM,SAAS,IAAI,OAAO,oBAAC,YAAQ,mBAAQ,IAAY;AACvD,UAAM,SAAS,IAAI,SAAS,oBAAC,QAAI,kBAAO,IAAQ;AAChD,QAAI,IAAI,MAAM;AACZ,aACE,oBAAC,aAAsB,MAAM,IAAI,MAC9B,oBADa,KAEhB;AAAA,IAEJ;AACA,WAAO,oBAAC,UAAkB,oBAAR,KAAe;AAAA,EACnC,CAAC,GACH;AAEJ;AAGA,IAAM,cAA4C,CAAC;AAGnD,SAAS,UAAU,KAAwB,OAAe,UAAuC;AAC/F,MAAI,aAAa,OAAW,QAAO;AACnC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;AACjC,eAAW,IAAI,CAAC,GAAG,UAAU,KAAK,oBAAoB;AAAA,EACxD;AACA,SAAO;AACT;AAEA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,UAAU,CAAC,UACf,MAAM,KAAK,CAAC,SAAS,MAAM,KAAK,GAAG,CAAC,GAAG;AAEzC,MAAI,MAAM,SAAS,WAAW;AAC5B;AAAA;AAAA;AAAA,MAGE;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,QAAQ,mBAAmB,CAAC,CAAC;AAAA,UAElE,8BAAC,QAAK,MAAM,MAAM,MAAM,OAAc,OAAO,QAAQ,MAAM,IAAI,GAAG;AAAA;AAAA,MACpE;AAAA;AAAA,EAEJ;AACA,MAAI,MAAM,SAAS,aAAa;AAC9B,WACE,oBAAC,aAAU,WAAU,uBACnB,8BAAC,QAAK,MAAM,MAAM,MAAM,OAAc,OAAO,QAAQ,MAAM,IAAI,GAAG,GACpE;AAAA,EAEJ;AACA,MAAI,MAAM,SAAS,QAAQ;AACzB,WACE,oBAAC,aAAU,SAAS,MAAM,SACvB,gBAAM,MAAM,IAAI,CAAC,MAAM,UAAU;AAGhC,YAAM,QAAQ,QAAQ,CAAC,QAAQ,IAAI,SAAS,KAAK;AACjD,aACE,oBAAC,iBACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN;AAAA,UACA,OAAO,UAAU,SAAY,SAAY,QAAQ,iBAAiB;AAAA;AAAA,MACpE,KALkB,KAMpB;AAAA,IAEJ,CAAC,GACH;AAAA,EAEJ;AACA,MAAI,MAAM,SAAS,SAAS;AAC1B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,MAAM;AAAA,QAIX,KAAK,MAAM,OAAO;AAAA,QACjB,GAAI,MAAM,MAAM,CAAC,IAAI,EAAE,eAAe,KAAK;AAAA,QAC5C,WAAU;AAAA;AAAA,IACZ;AAAA,EAEJ;AACA,QAAM,YAAY,QAAQ,CAAC,QAAQ,IAAI,QAAQ,aAAa;AAC5D,SACE,qBAAC,SAAM,WAAU,QACd;AAAA,UAAM,QACL,oBAAC,eACC,8BAAC,YACE,gBAAM,KAAK,IAAI,CAAC,MAAM,UACrB,oBAAC,aAAsB,OAAM,OAC3B;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN;AAAA,QACA,OAAO,UAAU,MAAM,QAAQ,CAAC,GAAG,OAAO,SAAS;AAAA;AAAA,IACrD,KALc,KAMhB,CACD,GACH,GACF;AAAA,IAEF,oBAAC,aACE,gBAAM,KAAK,IAAI,CAAC,KAAK,aAAa;AACjC,YAAM,WAAW,QAAQ,CAAC,QAAQ,IAAI,QAAQ,QAAQ;AACtD,aACE,oBAAC,YACE,cAAI,IAAI,CAAC,MAAM,cACd,oBAAC,aAA0B,WAAU,iCACnC;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN;AAAA,UACA,OAAO,UAAU,KAAK,WAAW,QAAQ;AAAA;AAAA,MAC3C,KALc,SAMhB,CACD,KATY,QAUf;AAAA,IAEJ,CAAC,GACH;AAAA,KACF;AAEJ;AAEA,SAAS,aAAa;AAAA,EACpB,UAAU;AAAA,EACV;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,OAAO;AACb,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,YAAY,OAAoB,IAAI;AAE1C,QAAM,QAAQ;AAAA,IACZ,MAAM,aAAa,YAAY,KAAK,MAAM,UAAU,CAAC;AAAA,IACrD,CAAC,YAAY,KAAK,IAAI;AAAA,EACxB;AAIA,QAAM,eAAe,QAAQ,MAAM;AACjC,UAAM,MAAM,oBAAI,IAAiC;AACjD,eAAW,QAAQ,KAAK,WAAW,SAAS,CAAC,GAAG;AAC9C,YAAM,OAAO,IAAI,IAAI,KAAK,IAAI,KAAK;AACnC,UAAI,KAAM,MAAK,KAAK,IAAI;AAAA,UACnB,KAAI,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;AAAA,IACrC;AACA,WAAO;AAAA,EACT,GAAG,CAAC,KAAK,SAAS,CAAC;AAEnB,mCAAiC,WAAW,iBAAiB;AAE7D,MAAI,KAAK,OAAO,WAAW,GAAG;AAC5B,WACE,oBAAC,SAAI,WAAW,GAAG,+CAA+C,SAAS,GACzE,8BAAC,cAAW,MAAK,SAAQ,OAAO,EAAE,mBAAmB,GAAG,GAC1D;AAAA,EAEJ;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAME,oBAAC,aAAQ,KAAK,WAAW,WAAW,GAAG,iCAAiC,SAAS,GAC9E,eAAK,OAAO,IAAI,CAAC,OAAO,UACvB;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,aAAa,IAAI,KAAK,KAAK;AAAA;AAAA,MAJ7B;AAAA,IAKP,CACD,GACH;AAAA;AAEJ;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,QAAQ,MAAM,IAAI,YAAY;AAAA,EAC9B,UAAU;AACZ;AAEA,IAAO,uBAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/image/image-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Image adapter — the reference implementation of \"adapters emit data\".\n *\n * It renders an `<img>`, never a `<canvas>`. anyview draws images to a canvas\n * (`ImageAdapter.ts:118`, `PageRenderer.tsx:128`), which throws away the alt\n * text, the browser's own decoding and zoom, and the ability to select or save\n * the image — with no route back, because its API has no alt-text path at all.\n * Here the description travels on the `FileSource` itself, so a screen reader\n * user gets the same information a sighted user does.\n */\n\nimport { cn, StatePanel, useLocale } from \"@elabs-ai/components-ui\";\nimport { useEffect, useState } from \"react\";\n\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { imageManifest } from \"./image-manifest\";\nimport { toViewerError } from \"../../core/errors\";\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\n\nexport interface ImageDocument extends AdapterDocument {\n kind: \"image\";\n /** A URL an `<img>` can load — the source's own URL, or a minted object URL. */\n url: string;\n /** Intrinsic size, once decoded. Used to reserve the box and to label the file. */\n width?: number;\n height?: number;\n}\n\n/** Decode just enough to learn the intrinsic size. Never rejects the load. */\nfunction measure(\n url: string,\n signal?: AbortSignal,\n): Promise<{ width: number; height: number } | undefined> {\n if (typeof Image !== \"function\") return Promise.resolve(undefined);\n return new Promise((resolve) => {\n const img = new Image();\n const done = (value?: { width: number; height: number }) => {\n img.onload = null;\n img.onerror = null;\n resolve(value);\n };\n img.onload = () => done({ width: img.naturalWidth, height: img.naturalHeight });\n // A measurement failure is not a load failure — the <img> will surface it.\n img.onerror = () => done(undefined);\n signal?.addEventListener(\"abort\", () => done(undefined), { once: true });\n img.src = url;\n });\n}\n\nclass ImageAdapter implements FileAdapter {\n #source?: ResolvedFileSource;\n\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<ImageDocument> {\n this.#source = source;\n try {\n const url = await source.url(context.signal);\n const size = await measure(url, context.signal);\n return { kind: \"image\", url, ...size };\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n }\n\n dispose(): void {\n // Releases the object URL this load minted, if any. A remote public image\n // never minted one, and `revoke()` is a no-op there.\n this.#source?.revoke();\n this.#source = undefined;\n }\n}\n\nfunction ImageRenderer({\n document: doc,\n source,\n className,\n zoom = \"fit-page\",\n rotation = 0,\n}: AdapterRendererProps) {\n const image = doc as ImageDocument;\n const { t } = useLocale();\n const [failed, setFailed] = useState(false);\n\n useEffect(() => setFailed(false), [image.url]);\n\n if (failed) {\n // A terminal, settled failure (loading-states.md): the browser tried and\n // gave up, so this is not a transient not-ready state.\n return (\n <div className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}>\n <StatePanel\n kind=\"error\"\n title={t(\"viewer.error.imageFailedTitle\")}\n description={t(\"viewer.error.imageFailed\", { name: source.name })}\n />\n </div>\n );\n }\n\n const fitting = typeof zoom !== \"number\";\n // A quarter turn swaps the axes: what was the image's height is now the width\n // the reader sees. Everything below that treats 90/270 differently is that one\n // fact — 180 leaves the bounds alone and needs none of it.\n const quarter = rotation === 90 || rotation === 270;\n const scaled =\n fitting || image.width === undefined || image.height === undefined\n ? undefined\n : { width: image.width * zoom, height: image.height * zoom };\n // A `transform` does not change layout, so a turned image at a fixed scale\n // needs a box with the ROTATED bounds. Without one it overflows the pane on\n // every side, and the half above the top edge is unreachable — scrolling only\n // ever reaches transform overflow past the END edges.\n const boxed = quarter && scaled !== undefined;\n\n const img = (\n <img\n src={image.url}\n // An image with no author description is decorative to AT — an empty alt\n // is correct and deliberate, not a missing label.\n alt={source.alt ?? \"\"}\n width={image.width}\n height={image.height}\n onError={() => setFailed(true)}\n className={cn(\n \"block object-contain\",\n !quarter && zoom === \"fit-page\" && \"max-h-full max-w-full\",\n !quarter && zoom === \"fit-width\" && \"h-auto max-w-full\",\n // Turned on its side, the pane's HEIGHT caps the image's width and its\n // width caps the height. Container units read the pane directly, so the\n // fit needs no measurement. Both fit modes converge here deliberately:\n // an image whose rotated width filled the pane would overflow the top\n // edge as well, which is the unreachable half described above.\n fitting && quarter && \"max-h-[100cqw] max-w-[100cqh]\",\n // At a fixed scale the intrinsic cap has to come off, or \"200%\" would\n // silently stop at the pane's width.\n !fitting && \"max-w-none\",\n boxed && \"absolute top-1/2 left-1/2\",\n )}\n style={{\n ...scaled,\n ...(rotation === 0\n ? undefined\n : {\n transform: boxed\n ? `translate(-50%, -50%) rotate(${rotation}deg)`\n : `rotate(${rotation}deg)`,\n }),\n }}\n />\n );\n\n return (\n // Centred on both axes: a small image pinned to the top-left of a tall pane\n // reads as a layout accident. While fitting, the box takes the pane's height\n // so `max-h-full` has something definite to resolve against; at a fixed\n // scale it grows instead, and `FileViewerContent` scrolls it.\n <div\n className={cn(\n \"flex items-center justify-center\",\n fitting ? \"h-full\" : \"min-h-full\",\n // Only a size container can answer `cqh`, and only a definite height\n // makes one — which is exactly the fitting case.\n fitting && quarter && \"[container-type:size]\",\n className,\n )}\n >\n {quarter && scaled ? (\n <div className=\"relative shrink-0\" style={{ width: scaled.height, height: scaled.width }}>\n {img}\n </div>\n ) : (\n img\n )}\n </div>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: imageManifest,\n create: () => new ImageAdapter(),\n Renderer: ImageRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/json/json-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * JSON adapter — parses to a real value and renders it as a collapsible tree.\n *\n * Why a tree and not a pretty-printed `<pre>`: the whole reason to open a JSON\n * file in a viewer rather than an editor is to find something in it. A tree\n * collapses the parts you are not reading, and `@elabs-ai/components-ui`'s\n * `Tree` already ships the keyboard model, roving tabindex and virtualization,\n * so this adapter contributes the MAPPING and nothing else.\n *\n * Invalid JSON is a terminal `parse-failed`, reported with the position the\n * engine gave us — not a silent fall-through to plain text, which would hide\n * the one fact the user needs.\n */\n\nimport { cn, Tree, type TreeNode, useLocale } from \"@elabs-ai/components-ui\";\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\n\nimport { ViewerError, toViewerError } from \"../../core/errors\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { jsonManifest } from \"./json-manifest\";\n\nexport interface JsonDocument extends AdapterDocument {\n kind: \"json\";\n /** The parsed value. `unknown` on purpose — it is whatever the file held. */\n value: unknown;\n /** The original source text, for the raw view and for copy. */\n text: string;\n}\n\n/** A one-line preview of a value, used as the collapsed label for a container. */\nfunction summarize(value: unknown): string {\n if (Array.isArray(value)) return `[${String(value.length)}]`;\n if (value !== null && typeof value === \"object\") {\n return `{${String(Object.keys(value).length)}}`;\n }\n if (typeof value === \"string\") return `\"${value}\"`;\n return String(value);\n}\n\n/** True for a value that has children worth expanding. */\nfunction isContainer(value: unknown): value is Record<string, unknown> | unknown[] {\n return value !== null && typeof value === \"object\";\n}\n\n/**\n * Map a parsed value onto `Tree` nodes.\n *\n * Ids are the JSON path (`$.items[2].name`), which makes them stable across\n * re-renders and directly usable as a \"reveal this node\" target later.\n */\nexport function toTreeNodes(value: unknown, path = \"$\"): TreeNode[] {\n if (!isContainer(value)) return [];\n\n const entries: [string, unknown][] = Array.isArray(value)\n ? value.map((item, index) => [String(index), item])\n : Object.entries(value);\n\n return entries.map(([key, child]) => {\n const childPath = Array.isArray(value) ? `${path}[${key}]` : `${path}.${key}`;\n const container = isContainer(child);\n return {\n id: childPath,\n label: (\n <span className=\"font-mono\">\n <span className=\"text-foreground\">{key}</span>\n <span className=\"text-muted-foreground\">: </span>\n <span className={container ? \"text-muted-foreground\" : \"text-info-text\"}>\n {summarize(child)}\n </span>\n </span>\n ),\n children: container ? toTreeNodes(child, childPath) : undefined,\n };\n });\n}\n\nclass JsonAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<JsonDocument> {\n let text: string;\n try {\n text = await source.text(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n try {\n return { kind: \"json\", value: JSON.parse(text) as unknown, text };\n } catch (error) {\n throw new ViewerError(\n \"parse-failed\",\n error instanceof Error ? error.message : \"Invalid JSON.\",\n { fileName: source.name, cause: error },\n );\n }\n }\n}\n\nfunction JsonRenderer({ document: doc, className }: AdapterRendererProps) {\n const json = doc as JsonDocument;\n const { t } = useLocale();\n const nodes = toTreeNodes(json.value);\n\n // A scalar top level (`42`, `\"hello\"`, `null`) has no children to expand —\n // a tree of nothing would read as an empty state, which it is not.\n if (nodes.length === 0) {\n return (\n <pre className={cn(\"text-code font-mono break-words whitespace-pre-wrap\", className)}>\n {json.text}\n </pre>\n );\n }\n\n return (\n <Tree\n aria-label={t(\"viewer.json.tree\")}\n nodes={nodes}\n // The top level open, everything below collapsed: enough to see the shape\n // without paying to build a DOM for a deeply nested document.\n defaultExpandedIds={nodes.map((node) => node.id)}\n selectionMode=\"none\"\n virtualize\n className={cn(\"h-full overflow-auto\", className)}\n />\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: jsonManifest,\n create: () => new JsonAdapter(),\n Renderer: JsonRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":";;;;;;;;;;;AAgBA,SAAS,IAAI,MAAqB,iBAAiB;AAuD3C,SACE,KADF;AAjCR,SAAS,UAAU,OAAwB;AACzC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,IAAI,OAAO,MAAM,MAAM,CAAC;AACzD,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,WAAO,IAAI,OAAO,OAAO,KAAK,KAAK,EAAE,MAAM,CAAC;AAAA,EAC9C;AACA,MAAI,OAAO,UAAU,SAAU,QAAO,IAAI,KAAK;AAC/C,SAAO,OAAO,KAAK;AACrB;AAGA,SAAS,YAAY,OAA8D;AACjF,SAAO,UAAU,QAAQ,OAAO,UAAU;AAC5C;AAQO,SAAS,YAAY,OAAgB,OAAO,KAAiB;AAClE,MAAI,CAAC,YAAY,KAAK,EAAG,QAAO,CAAC;AAEjC,QAAM,UAA+B,MAAM,QAAQ,KAAK,IACpD,MAAM,IAAI,CAAC,MAAM,UAAU,CAAC,OAAO,KAAK,GAAG,IAAI,CAAC,IAChD,OAAO,QAAQ,KAAK;AAExB,SAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AACnC,UAAM,YAAY,MAAM,QAAQ,KAAK,IAAI,GAAG,IAAI,IAAI,GAAG,MAAM,GAAG,IAAI,IAAI,GAAG;AAC3E,UAAM,YAAY,YAAY,KAAK;AACnC,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OACE,qBAAC,UAAK,WAAU,aACd;AAAA,4BAAC,UAAK,WAAU,mBAAmB,eAAI;AAAA,QACvC,oBAAC,UAAK,WAAU,yBAAwB,gBAAE;AAAA,QAC1C,oBAAC,UAAK,WAAW,YAAY,0BAA0B,kBACpD,oBAAU,KAAK,GAClB;AAAA,SACF;AAAA,MAEF,UAAU,YAAY,YAAY,OAAO,SAAS,IAAI;AAAA,IACxD;AAAA,EACF,CAAC;AACH;AAEA,IAAM,cAAN,MAAyC;AAAA,EACvC,MAAM,KAAK,QAA4B,SAAoD;AACzF,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,OAAO,KAAK,QAAQ,MAAM;AAAA,IACzC,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,eAAe,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACrE;AAEA,QAAI;AACF,aAAO,EAAE,MAAM,QAAQ,OAAO,KAAK,MAAM,IAAI,GAAc,KAAK;AAAA,IAClE,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR;AAAA,QACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACzC,EAAE,UAAU,OAAO,MAAM,OAAO,MAAM;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,aAAa,EAAE,UAAU,KAAK,UAAU,GAAyB;AACxE,QAAM,OAAO;AACb,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,QAAQ,YAAY,KAAK,KAAK;AAIpC,MAAI,MAAM,WAAW,GAAG;AACtB,WACE,oBAAC,SAAI,WAAW,GAAG,uDAAuD,SAAS,GAChF,eAAK,MACR;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,EAAE,kBAAkB;AAAA,MAChC;AAAA,MAGA,oBAAoB,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,MAC/C,eAAc;AAAA,MACd,YAAU;AAAA,MACV,WAAW,GAAG,wBAAwB,SAAS;AAAA;AAAA,EACjD;AAEJ;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,QAAQ,MAAM,IAAI,YAAY;AAAA,EAC9B,UAAU;AACZ;AAEA,IAAO,uBAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/markdown/markdown-adapter.tsx","../src/adapters/markdown/markdown-marks.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Markdown adapter — a `.md` file read as a DOCUMENT, not as source.\n *\n * Renders through `streamdown`, the same markdown engine\n * `@elabs-ai/components-ai`'s `MarkdownView` and\n * `@elabs-ai/components-editor`'s preview use, mapped onto the same\n * `Prose*` primitives from `@elabs-ai/components-ui`. That is the\n * point: a README must not look like three different documents depending on\n * which pane it opened in. The element map below is deliberately a near-copy of\n * `MarkdownView`'s — the packages are siblings and may not import each other,\n * and the shared half that could move down (the `Prose*` set, the Streamdown\n * locale bridge) already has.\n *\n * What this does NOT do, on purpose:\n *\n * - **No plugins.** `@streamdown/code`, `/math`, `/cjk` and `/mermaid` are four\n * more packages a consumer would have to install to open one file. Fenced code\n * is rendered as an unhighlighted block by the element map below (Streamdown\n * emits a bare `<code>` without the code plugin, so the `<pre>` is ours); a\n * source file opened directly still gets Shiki from the `code` adapter, and a\n * diagram fence reads as its own text rather than as a picture.\n * - **No streaming.** A file arrives settled or not at all, so `mode=\"static\"`\n * and `parseIncompleteMarkdown={false}` — the incomplete-token repair that\n * makes Streamdown good at chat is only overhead for a file on disk.\n */\n\nimport {\n ProseBlockquote,\n ProseHeading,\n ProseInlineCode,\n ProseLink,\n ProseList,\n ProseListItem,\n ProseText,\n StatePanel,\n cn,\n useLocale,\n useStreamdownTranslations,\n} from \"@elabs-ai/components-ui\";\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { createContext, use, useMemo, useRef } from \"react\";\nimport type * as StreamdownExports from \"streamdown\";\n\nimport { toViewerError } from \"../../core/errors\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { blockMark, blockMarkAttributes, type PositionedNode } from \"./markdown-marks\";\nimport { markdownManifest } from \"./markdown-manifest\";\n\n/**\n * Characters kept before truncation. Markdown is parsed into an element tree,\n * which costs more than a `<pre>` — this sits between the code and plain-text\n * limits for that reason.\n */\nexport const MARKDOWN_CHARACTER_LIMIT = 1_000_000;\n\nexport interface MarkdownDocument extends AdapterDocument {\n kind: \"markdown\";\n text: string;\n /** Total characters in the file, when more than what is shown. */\n totalCharacters?: number;\n}\n\n// Type-only, so it erases: the runtime edge is the dynamic import below.\ntype StreamdownModule = typeof StreamdownExports;\ntype StreamdownComponents = NonNullable<\n ComponentProps<StreamdownModule[\"Streamdown\"]>[\"components\"]\n>;\n\n/**\n * The engine, resolved once per page.\n *\n * Loaded in `load()` rather than imported at the top of this module for the\n * reason every other adapter does it: `streamdown` is an OPTIONAL peer, and a\n * static edge would make this module unresolvable — not merely heavier — for a\n * consumer who never installs it. The renderer reads the same cached module,\n * which is guaranteed to be populated because a document only exists once\n * `load()` has resolved.\n */\nlet streamdown: StreamdownModule | undefined;\n\nasync function loadStreamdown(): Promise<StreamdownModule> {\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n streamdown ??= await import(\"streamdown\");\n return streamdown;\n}\n\nclass MarkdownAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<MarkdownDocument> {\n let raw: string;\n try {\n raw = await source.text(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Before returning: a document this adapter's Renderer cannot draw is a\n // failed load, not a blank pane. A missing peer surfaces from here as\n // `parser-missing` (see `parserMissingError`).\n await loadStreamdown();\n\n const truncated = raw.length > MARKDOWN_CHARACTER_LIMIT;\n return {\n kind: \"markdown\",\n text: truncated ? raw.slice(0, MARKDOWN_CHARACTER_LIMIT) : raw,\n totalCharacters: truncated ? raw.length : undefined,\n };\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\n/** Never past `h6`, whatever the offset — an `h7` is not an element. */\nconst clampHeadingLevel = (level: number): ProseHeadingLevel =>\n Math.min(6, Math.max(1, level)) as ProseHeadingLevel;\n\n/* -------------------------------------------------------------------------- */\n/* Highlight plates */\n/* -------------------------------------------------------------------------- */\n\nconst EMPTY_MARKS: MarkRanges = { ranges: [], activeIndex: -1 };\n\n/**\n * The marks, read by the block components.\n *\n * A context rather than a closure over `buildProseComponents`, so the element\n * map stays memoized on the heading offset alone: rebuilding it per keystroke\n * of a find-as-you-type would hand Streamdown a new `components` object every\n * time, which it treats as a reason to re-render every block in the file.\n */\nconst MarkContext = createContext<MarkRanges>(EMPTY_MARKS);\n\n/** Set once a block has plated, so a nested one does not plate on top of it. */\nconst PlatedContext = createContext(false);\n\n/**\n * The plate for one block, and a wrapper that stops its children plating again.\n *\n * A loose list item renders as `li > p`, and both nodes' source spans contain\n * the same mark — without the suppression the reader sees two stacked fills for\n * one citation.\n */\nfunction usePlate(node: PositionedNode | undefined, className?: string) {\n const marks = use(MarkContext);\n const plated = use(PlatedContext);\n const mark = plated ? { marked: false, active: false } : blockMark(marks, node);\n return {\n props: blockMarkAttributes(mark, className),\n wrap: (children: ReactNode) =>\n mark.marked ? <PlatedContext value={true}>{children}</PlatedContext> : children,\n };\n}\n\n/*\n * The plating blocks are named components rather than inline arrows in the map\n * below: they call a hook, and a lowercase `p:` key is not a component name as\n * far as the rules-of-hooks lint can tell.\n */\n\nconst MarkdownParagraph: NonNullable<StreamdownComponents[\"p\"]> = ({\n node,\n className,\n children,\n ...props\n}) => {\n const plate = usePlate(node, className);\n return (\n <ProseText {...plate.props} {...props}>\n {plate.wrap(children)}\n </ProseText>\n );\n};\n\nconst MarkdownListItem: NonNullable<StreamdownComponents[\"li\"]> = ({\n node,\n className,\n children,\n ...props\n}) => {\n const plate = usePlate(node, className);\n return (\n <ProseListItem {...plate.props} {...props}>\n {plate.wrap(children)}\n </ProseListItem>\n );\n};\n\nconst MarkdownCode: NonNullable<StreamdownComponents[\"code\"]> = ({\n node,\n className,\n children,\n ...props\n}) => {\n const fence = /\\blanguage-/.test(className ?? \"\");\n // The plate goes on the `<pre>` — the whole fence is what a citation points\n // at. Its own `bg-surface-muted` wins over the plate's fill (see\n // `blockMarkAttributes`), so the code keeps its ground and the rail is what\n // says it is cited.\n const plate = usePlate(\n fence ? node : undefined,\n \"bg-surface-muted text-code overflow-x-auto rounded-md p-3 font-mono whitespace-pre\",\n );\n // A fence needs its own `<pre>`. With no `@streamdown/code` plugin installed\n // Streamdown emits a BARE `<code data-block>` — no block wrapper — which the\n // surrounding prose's `whitespace-normal` then collapses into one run-on\n // line. The fence is unhighlighted here by design (see the module docblock);\n // it still has to read as a block of code.\n return fence ? (\n <pre {...plate.props}>\n <code className={className} {...props}>\n {plate.wrap(children)}\n </code>\n </pre>\n ) : (\n <ProseInlineCode className={className} {...props}>\n {children}\n </ProseInlineCode>\n );\n};\n\n/** Everything that does not depend on the heading offset. */\nconst SHARED_COMPONENTS: StreamdownComponents = {\n p: MarkdownParagraph,\n a: ({ node: _node, ...props }) => <ProseLink {...props} />,\n // `ref` is stripped: ProseList's intersection ref type (ul & ol) is narrower\n // than the per-element ref react-markdown passes.\n ul: ({ node: _node, ref: _ref, ...props }) => <ProseList {...props} />,\n ol: ({ node: _node, ref: _ref, ...props }) => <ProseList ordered {...props} />,\n li: MarkdownListItem,\n blockquote: ({ node: _node, ...props }) => <ProseBlockquote {...props} />,\n code: MarkdownCode,\n};\n\n/**\n * Map the markdown element tree onto the Prose primitives.\n *\n * Built per `baseHeadingLevel` (memoized by the renderer, not rebuilt each\n * render): the map is otherwise pure and Streamdown puts it into a context, so\n * a fresh object per render would re-render every block.\n *\n * The heading offset is the reason this takes an argument at all. A viewed file\n * carries its own heading tree, correct only relative to the page hosting it —\n * a README's `#` inside an app that already has an `<h1>` would otherwise put\n * two `h1`s in a screen reader's flat heading list. Same seam, same arithmetic\n * as `@elabs-ai/components-ai`'s `MarkdownView`.\n */\nfunction buildProseComponents(baseHeadingLevel: ProseHeadingLevel): StreamdownComponents {\n const heading =\n (markdownLevel: number): StreamdownComponents[\"h1\"] =>\n ({ node, className, children, ...props }) => {\n const plate = usePlate(node, className);\n return (\n <ProseHeading\n level={clampHeadingLevel(markdownLevel + baseHeadingLevel - 1)}\n {...plate.props}\n {...props}\n >\n {plate.wrap(children)}\n </ProseHeading>\n );\n };\n return {\n h1: heading(1),\n h2: heading(2),\n h3: heading(3),\n h4: heading(4),\n h5: heading(5),\n h6: heading(6),\n ...SHARED_COMPONENTS,\n };\n}\n\nfunction MarkdownRenderer({\n document: doc,\n className,\n baseHeadingLevel = 2,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const markdown = doc as MarkdownDocument;\n const { t, formatNumber } = useLocale();\n const translations = useStreamdownTranslations();\n const components = useMemo(() => buildProseComponents(baseHeadingLevel), [baseHeadingLevel]);\n const container = useRef<HTMLDivElement>(null);\n const marks = useMemo(\n () => toMarkRanges(highlights, markdown.text.length),\n [highlights, markdown.text],\n );\n\n useScrollActiveHighlightIntoView(container, activeHighlightId);\n\n if (markdown.text.trim().length === 0) {\n return <StatePanel kind=\"empty\" title={t(\"viewer.file.empty\")} className={className} />;\n }\n\n const Streamdown = streamdown?.Streamdown;\n if (!Streamdown) return null;\n\n return (\n // No `overflow-auto` here: `FileViewerContent` is the scroll boundary, and\n // nesting a second one clips the last block above the outer pane's padding.\n <div ref={container} className={cn(\"flex flex-col gap-2\", className)}>\n {markdown.totalCharacters !== undefined && (\n // Not an error — the file is fine, we are showing part of it.\n <p role=\"status\" className=\"text-meta text-muted-foreground\">\n {t(\"viewer.text.truncated\", {\n shown: formatNumber(markdown.text.length),\n total: formatNumber(markdown.totalCharacters),\n })}\n </p>\n )}\n {/* `max-w-prose` because this is genuine long-form reading, not a label:\n a README run edge-to-edge across a wide pane loses the line. The pane's\n own padding is the page margin — the article adds none. */}\n <article className=\"mx-auto max-w-prose space-y-3\">\n {/* `mode=\"static\"` is what makes the plates addressable: it hands the\n whole file to ONE parse, so every node's `position.start.offset` is\n an offset into `document.text`. Splitting into blocks (streaming\n mode) would restart them per block. */}\n <MarkContext value={marks}>\n <Streamdown\n mode=\"static\"\n parseIncompleteMarkdown={false}\n components={components}\n translations={translations}\n >\n {markdown.text}\n </Streamdown>\n </MarkContext>\n </article>\n </div>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: markdownManifest,\n create: () => new MarkdownAdapter(),\n Renderer: MarkdownRenderer,\n};\n\nexport default adapterModule;\n","/**\n * Markdown highlights are BLOCK-shaped, and that is a deliberate downgrade.\n *\n * Every other text adapter marks characters, because its `document.text` is the\n * thing on screen. Markdown's is not: `text` is the SOURCE, so offset 12 might\n * land in the middle of `**bold**` — two of whose characters are never drawn at\n * all — or inside a link's URL, which the reader only sees as a word. There is\n * no honest character-level mapping from source offsets to rendered text\n * without re-implementing the parser's own position bookkeeping for inline\n * nodes, and getting that subtly wrong would put marks on the wrong words while\n * looking exactly as confident as a correct one.\n *\n * So a markdown mark says \"this passage is in HERE\" — the paragraph, heading,\n * list item or fence whose source span the mark touches is plated whole. That\n * is enough for the case the feature exists for (a citation pointing a reader at\n * the passage an answer came from) and it cannot be subtly wrong: a block either\n * contains the source range or it does not.\n *\n * The offsets are trustworthy because the renderer runs Streamdown in\n * `mode=\"static\"`, which hands the WHOLE file to one parse — no per-block\n * splitting, no incomplete-token repair — so every hast node's\n * `position.start.offset` is an offset into `document.text` itself.\n */\n\nimport { cn } from \"@elabs-ai/components-ui\";\n\nimport type { MarkRanges } from \"../../core/highlight-marks\";\n\n/** The subset of a unist `Position` this needs. Structural, so no unist import. */\nexport interface SourcePosition {\n start?: { offset?: number };\n end?: { offset?: number };\n}\n\n/** A hast node as the component overrides receive it. */\nexport interface PositionedNode {\n position?: SourcePosition;\n}\n\n/** Whether a block carries any mark, and whether it carries the current one. */\nexport interface BlockMark {\n marked: boolean;\n active: boolean;\n}\n\nconst UNMARKED: BlockMark = { marked: false, active: false };\n\n/**\n * Which marks land inside one block's source span.\n *\n * Half-open on both sides and strict, so a mark ending exactly where a block\n * begins belongs to the block before it — otherwise the paragraph after a\n * marked heading would light up too, on every citation.\n */\nexport function blockMark(marks: MarkRanges, node: PositionedNode | undefined): BlockMark {\n const start = node?.position?.start?.offset;\n const end = node?.position?.end?.offset;\n if (start === undefined || end === undefined || end <= start) return UNMARKED;\n\n let marked = false;\n let active = false;\n marks.ranges.forEach(([from, to], index) => {\n if (from >= end || to <= start) return;\n marked = true;\n if (index === marks.activeIndex) active = true;\n });\n return marked ? { marked, active } : UNMARKED;\n}\n\n/**\n * The attributes a plated block carries.\n *\n * `data-slot`/`data-active` are the same seam the character marks use, so one\n * selector finds \"the current highlight\" whatever adapter drew it — that is what\n * `useScrollActiveHighlightIntoView` scrolls to. `aria-current` is the part that\n * is not decoration: the active block is announced as the current one rather\n * than being distinguishable only by a warmer plate (WCAG 1.4.1).\n */\nexport interface BlockMarkAttributes {\n \"data-slot\"?: \"highlight-block\";\n /**\n * Present-or-absent, never `\"false\"` — the same shape `MatchHighlight`'s marks\n * and the PDF's boxes emit, so one selector (`[data-active]`) finds the current\n * highlight whichever painter drew it.\n */\n \"data-active\"?: \"\";\n \"aria-current\"?: \"true\";\n className?: string;\n}\n\n/**\n * Plate classes.\n *\n * A marked block gets a fill AND a rail; the current one doubles the rail's\n * width on top of warming the fill, so \"which one am I on\" survives greyscale\n * and a colour-vision difference. The negative margins let the plate bleed into\n * the prose column's own gutter instead of shifting the text when it appears.\n */\nconst MARKED_CLASS = \"bg-highlight/40 border-s-2 border-highlight rounded-e-sm ps-2 -ms-2\";\nconst ACTIVE_CLASS =\n \"bg-highlight-active/40 border-s-4 border-highlight-active rounded-e-sm ps-2 -ms-2\";\n\n/**\n * Spreadable attributes for a block, given how the marks fell on it.\n *\n * The block's OWN classes are merged last and therefore win, which is the\n * deliberate answer for a fenced code block: `bg-surface-muted` beats the\n * plate's fill, so the code keeps its own ground and the rail alone says it is\n * cited. Leaving the order to CSS source order instead would make that outcome\n * accidental.\n */\nexport function blockMarkAttributes(mark: BlockMark, className?: string): BlockMarkAttributes {\n if (!mark.marked) return className === undefined ? {} : { className };\n return {\n \"data-slot\": \"highlight-block\",\n ...(mark.active ? { \"data-active\": \"\" as const, \"aria-current\": \"true\" as const } : {}),\n className: cn(mark.active ? ACTIVE_CLASS : MARKED_CLASS, className),\n };\n}\n"],"mappings":"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","names":["cn","cn"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/media/media-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Video / audio adapter — native elements, deliberately.\n *\n * `@elabs-ai/components-ai` ships a designed `AudioPlayer` built on\n * `media-chrome`, and this adapter does NOT reach for it: `ai` is a Layer-2\n * sibling, so importing it would be a sideways edge the dependency gate rejects\n * (and would pull a chat package into a file viewer). Native `<video controls>`\n * / `<audio controls>` are the honest answer here — they bring the platform's\n * own accessible transport, keyboard handling, picture-in-picture, captions\n * menu and OS media keys, none of which a custom skin gets for free.\n *\n * The plan's \"extract `AudioPlayer` down to `ui`\" would let both sides share one\n * skinned player. That is a real refactor of an existing component with its own\n * tests and stories, so it is tracked separately rather than smuggled in here.\n */\n\nimport { cn, StatePanel, useLocale, type ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { useEffect, useState } from \"react\";\n\nimport { toViewerError } from \"../../core/errors\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { mediaManifest } from \"./media-manifest\";\n\nexport interface MediaDocument extends AdapterDocument {\n kind: \"media\";\n /** A URL the element can play — the source's own URL, or a minted object URL. */\n url: string;\n /** Which element to render. `resolveFileKind` already decided this. */\n media: \"video\" | \"audio\";\n}\n\nclass MediaAdapter implements FileAdapter {\n #source?: ResolvedFileSource;\n\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<MediaDocument> {\n this.#source = source;\n try {\n // Deliberately `url()`, never `bytes()`: an object URL lets the browser\n // stream and seek. Buffering a 2 GB video into memory to hand it to a\n // `<video>` element would be the wrong shape at every size.\n const url = await source.url(context.signal);\n return { kind: \"media\", url, media: source.category === \"audio\" ? \"audio\" : \"video\" };\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n }\n\n dispose(): void {\n this.#source?.revoke();\n this.#source = undefined;\n }\n}\n\nfunction MediaRenderer({ document: doc, source, className }: AdapterRendererProps) {\n const media = doc as MediaDocument;\n const { t } = useLocale();\n const [failed, setFailed] = useState(false);\n\n useEffect(() => setFailed(false), [media.url]);\n\n if (failed) {\n // Terminal: the browser has no decoder for this codec. Retrying cannot help,\n // so this states the fact rather than offering a button that does nothing.\n // The panel carries `role=\"alert\"` itself, and sits centred like every other\n // viewer state — a message pinned to the top-left of an empty pane reads as\n // a broken render.\n return (\n <div className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}>\n <StatePanel\n kind=\"error\"\n title={t(\"viewer.media.unsupportedTitle\")}\n description={t(\"viewer.media.unsupported\", { name: source.name })}\n />\n </div>\n );\n }\n\n const label = t(\"viewer.media.label\", { name: source.name });\n\n if (media.media === \"audio\") {\n return (\n <div className={cn(\"flex min-h-full items-center justify-center\", className)}>\n <audio\n src={media.url}\n controls\n aria-label={label}\n onError={() => setFailed(true)}\n className=\"w-full max-w-lg\"\n />\n </div>\n );\n }\n\n return (\n <div className={cn(\"flex min-h-full items-center justify-center\", className)}>\n <video\n src={media.url}\n controls\n // No autoplay: a viewer opens files the reader chose to look at, not to\n // listen to. Sound starting on its own is the reason browsers block it.\n aria-label={label}\n onError={() => setFailed(true)}\n className=\"max-h-full max-w-full\"\n />\n </div>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: mediaManifest,\n create: () => new MediaAdapter(),\n Renderer: MediaRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":";;;;;;;;;;AAkBA,SAAS,IAAI,YAAY,iBAA0C;AACnE,SAAS,WAAW,gBAAgB;AAyD5B;AArCR,IAAM,eAAN,MAA0C;AAAA,EACxC;AAAA,EAEA,MAAM,KAAK,QAA4B,SAAqD;AAC1F,SAAK,UAAU;AACf,QAAI;AAIF,YAAM,MAAM,MAAM,OAAO,IAAI,QAAQ,MAAM;AAC3C,aAAO,EAAE,MAAM,SAAS,KAAK,OAAO,OAAO,aAAa,UAAU,UAAU,QAAQ;AAAA,IACtF,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,eAAe,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACrE;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU;AAAA,EACjB;AACF;AAEA,SAAS,cAAc,EAAE,UAAU,KAAK,QAAQ,UAAU,GAAyB;AACjF,QAAM,QAAQ;AACd,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAE1C,YAAU,MAAM,UAAU,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;AAE7C,MAAI,QAAQ;AAMV,WACE,oBAAC,SAAI,WAAW,GAAG,+CAA+C,SAAS,GACzE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,EAAE,+BAA+B;AAAA,QACxC,aAAa,EAAE,4BAA4B,EAAE,MAAM,OAAO,KAAK,CAAC;AAAA;AAAA,IAClE,GACF;AAAA,EAEJ;AAEA,QAAM,QAAQ,EAAE,sBAAsB,EAAE,MAAM,OAAO,KAAK,CAAC;AAE3D,MAAI,MAAM,UAAU,SAAS;AAC3B,WACE,oBAAC,SAAI,WAAW,GAAG,+CAA+C,SAAS,GACzE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,MAAM;AAAA,QACX,UAAQ;AAAA,QACR,cAAY;AAAA,QACZ,SAAS,MAAM,UAAU,IAAI;AAAA,QAC7B,WAAU;AAAA;AAAA,IACZ,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,+CAA+C,SAAS,GACzE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,MAAM;AAAA,MACX,UAAQ;AAAA,MAGR,cAAY;AAAA,MACZ,SAAS,MAAM,UAAU,IAAI;AAAA,MAC7B,WAAU;AAAA;AAAA,EACZ,GACF;AAEJ;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,QAAQ,MAAM,IAAI,aAAa;AAAA,EAC/B,UAAU;AACZ;AAEA,IAAO,wBAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/pdf/pdf-adapter.tsx","../src/adapters/pdf/pdf-text.ts"],"sourcesContent":["\"use client\";\n\n/**\n * PDF adapter — the format nothing in this repo could open before.\n *\n * ## Why this one renders a canvas, and why that is not a rule break\n *\n * \"Adapters emit data, never HTML\" is about CHROME: an adapter may not smuggle\n * colours, fonts or borders past the token layer. A PDF page's pixels ARE the\n * document — a PDF is a description of marks on a page, and reproducing it is\n * the whole job. So the page is rasterized to a `<canvas>` in the file's own\n * colours, while everything AROUND it (the pager, the zoom control, the page\n * frame, the loading and error states) is brand-ui components on tokens.\n *\n * ## The text layer\n *\n * A canvas has no text, so a canvas-only PDF viewer is unusable: nothing to\n * select, copy, search or read aloud. pdf.js exposes the text with per-item\n * transforms, so we position transparent spans over the page. That is the same\n * approach pdf.js's own viewer takes, and it is what makes the page selectable\n * and reachable by a screen reader.\n *\n * ## Highlights are boxes, not marks\n *\n * Every other text adapter paints a `<mark>` around the cited characters. There\n * is no text to wrap here — the words are pixels — so a citation is a rectangle\n * over the raster, positioned from the page's own geometry (`pdf-text.ts`). The\n * same layer draws a `rect` address, which is why this is the one adapter that\n * can honour geometry a text pipeline never produced.\n *\n * ## The chrome is not here\n *\n * The pager and the zoom control used to live in this file, over `useState` no\n * one outside the canvas could reach. They are now shell parts over provider\n * state (ADR 0026): this renderer draws the page it is TOLD to, at the scale it\n * is told to, and reports back what a fit mode resolved to. It keeps its own\n * page only when mounted outside a provider.\n *\n * ## Pages scroll; they are not flipped\n *\n * The document is one continuous column of pages, virtualized over the shell's\n * viewport (`usePagedScroll`). A pager that replaced the canvas made a PDF the\n * only thing in the app you could not simply scroll, and it broke the two\n * gestures readers actually use — skimming, and dragging the scrollbar to\n * \"about two thirds in\". The page number is now a consequence of where the\n * reader is, and typing one is a scroll.\n */\n\nimport { cn, StatePanel, useLocale, type ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { toViewerError } from \"../../core/errors\";\nimport type { ResolvedHighlight } from \"../../core/highlight\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { createTextIndexBuilder, type TextIndex } from \"../../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport { usePageControl } from \"../../core/use-page-control\";\nimport { usePagedScroll } from \"../../core/use-paged-scroll\";\nimport { useViewportSize } from \"../../core/use-viewport-size\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n PageSize,\n} from \"../../core/types\";\nimport { DEFAULT_ZOOM } from \"../../core/zoom\";\nimport {\n openPdfDocument,\n type PdfDocument,\n type PdfPage,\n type PdfSession,\n type PdfTextItem,\n} from \"./pdf-engine\";\nimport { pdfManifest } from \"./pdf-manifest\";\nimport {\n PDF_PAGE_SEPARATOR,\n pageOfHighlight,\n pageTextChunk,\n rangeBoxes,\n rectBoxes,\n type PdfHighlightBox,\n type PdfTextSpan,\n} from \"./pdf-text\";\n\n/**\n * Breathing room kept around a fitted page, in CSS pixels.\n *\n * Without it a fit-to-width page is exactly as wide as the viewport, and the\n * page frame's own 1px border tips it over into a horizontal scrollbar — which\n * narrows the viewport, which re-fits smaller, which removes the scrollbar.\n */\nconst FIT_GUTTER = 16;\n\n/** Sanity bounds on a fitted scale, so a degenerate measurement cannot blank the page. */\nconst MIN_SCALE = 0.1;\nconst MAX_SCALE = 10;\n\n/**\n * Pages whose text is extracted for the plain-text projection.\n *\n * Extraction costs one worker round-trip per page, so doing it for a 900-page\n * document at load time would stall the first paint for something most readers\n * never use. The pages a reader actually looks at get their text layer rendered\n * on demand regardless — this cap only bounds the eager `document.text`.\n */\nexport const PDF_TEXT_PAGE_LIMIT = 50;\n\nexport interface PdfViewerDocument extends AdapterDocument {\n kind: \"pdf\";\n /** The live pdf.js handle. Owned by the adapter — the renderer only reads it. */\n handle: PdfDocument;\n pageCount: number;\n /** Page number per stretch of the projection, so a citation can turn the page. */\n textIndex?: TextIndex<number>;\n}\n\n/** Vertical space between two pages in the scrolling column, in CSS pixels. */\nconst PAGE_GAP = 16;\n\n/** The page frame's own 1px border, top and bottom — part of the reserved height. */\nconst PAGE_BORDER = 2;\n\n/**\n * Height assumed for a page whose size was never measured, in CSS pixels.\n *\n * Only reached by a document with no extractable page at all; every real one\n * contributes at least page 1 to `pageSizes`. US Letter at 96dpi.\n */\nconst FALLBACK_PAGE_HEIGHT = 1056;\n\nclass PdfAdapter implements FileAdapter {\n #session?: PdfSession;\n\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<PdfViewerDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n try {\n // pdf.js takes ownership of the buffer it is handed (it transfers it to\n // the worker), so the view is built here rather than reusing the source's\n // memoized copy in place.\n const session = await openPdfDocument(new Uint8Array(buffer), context.signal);\n this.#session = session;\n const handle = session.document;\n const { textIndex, pageSizes } = await extractText(handle, context.signal);\n return {\n kind: \"pdf\",\n handle,\n pageCount: handle.numPages,\n pageSizes,\n text: textIndex?.text,\n textIndex,\n // So a passage the shell cannot find in a 900-page report reads as \"past\n // the pages we extracted\" rather than \"not in this document\".\n textTruncated: handle.numPages > PDF_TEXT_PAGE_LIMIT || undefined,\n };\n } catch (error) {\n // A password-protected or corrupt file lands here; the engine failing to\n // resolve is caught upstream and becomes `parser-missing`.\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n\n dispose(): void {\n // Tears down the worker and its transferred buffers. Skipping this leaks a\n // worker per document opened, which a file browser hits within minutes.\n // The teardown lives on the SESSION, not on the document — see `PdfSession`.\n void this.#session?.destroy();\n this.#session = undefined;\n }\n}\n\n/**\n * Best-effort plain text, plus the page each stretch of it came from — and each\n * page's size, which the same loop already has in hand.\n *\n * Never throws: a document whose text will not extract still renders — the\n * reader loses search and citations, not the file. Whatever was extracted before\n * the failure is kept, because half a projection still answers half the queries.\n *\n * The index is built with the SAME join rule the renderer re-applies per page\n * (`pageTextChunk`), so an offset in the projection and a box on the page can\n * never disagree about which item they mean.\n *\n * The sizes are collected here rather than in their own pass because this loop\n * has already paid for `getPage` — a second pass over a 900-page document would\n * be 900 worker round-trips for numbers we are holding.\n */\nasync function extractText(\n handle: PdfDocument,\n signal?: AbortSignal,\n): Promise<{ textIndex?: TextIndex<number>; pageSizes?: PageSize[] }> {\n const pages = Math.min(handle.numPages, PDF_TEXT_PAGE_LIMIT);\n const builder = createTextIndexBuilder<number>({ separator: PDF_PAGE_SEPARATOR });\n const pageSizes: PageSize[] = [];\n try {\n for (let pageNumber = 1; pageNumber <= pages; pageNumber += 1) {\n if (signal?.aborted) break;\n const page = await handle.getPage(pageNumber);\n const { width, height } = page.getViewport({ scale: 1 });\n pageSizes.push({ width, height });\n const content = await page.getTextContent();\n builder.push(pageTextChunk(content.items).text, pageNumber);\n }\n } catch {\n // Fall through: return what we have.\n }\n const index = builder.build();\n return {\n textIndex: index.text.length > 0 ? index : undefined,\n pageSizes: pageSizes.length > 0 ? pageSizes : undefined,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\nfunction PdfRenderer({\n document: doc,\n source,\n className,\n highlights,\n activeHighlightId,\n pageNumber: pageNumberProp,\n onPageChange,\n zoom = DEFAULT_ZOOM,\n onZoomResolved,\n}: AdapterRendererProps) {\n const pdf = doc as PdfViewerDocument;\n\n const [pageNumber, goToPage] = usePageControl(pageNumberProp, onPageChange, pdf.pageCount);\n\n const listRef = useRef<HTMLDivElement>(null);\n const viewport = useViewportSize(listRef);\n\n /**\n * A page's size at scale 1.\n *\n * Falls back to the first page for anything past what the adapter measured\n * (`AdapterDocument.pageSizes` may be shorter than the page count). Uniform\n * page size is overwhelmingly the common case, and a wrong guess costs a\n * corrected scrollbar, not a wrong render.\n */\n const naturalAt = useCallback(\n (index: number): PageSize | undefined => pdf.pageSizes?.[index] ?? pdf.pageSizes?.[0],\n [pdf.pageSizes],\n );\n\n // A fit mode is a REQUEST the renderer resolves — it is the only party that\n // knows how wide its viewport is (`AdapterRendererProps.zoom`). Fitted to the\n // page the reader is ON, so a landscape page in a portrait document does not\n // re-fit the whole column.\n const natural = naturalAt(pageNumber - 1);\n const scale = useMemo(() => {\n if (typeof zoom === \"number\") return zoom;\n // Until the page and the pane have both been measured, 100% is a better\n // first frame than a guess that jumps.\n if (!natural || !viewport) return DEFAULT_ZOOM;\n const byWidth = (viewport.width - FIT_GUTTER) / natural.width;\n const fitted =\n zoom === \"fit-width\"\n ? byWidth\n : Math.min(byWidth, (viewport.height - FIT_GUTTER) / natural.height);\n return Math.min(MAX_SCALE, Math.max(MIN_SCALE, fitted));\n }, [zoom, natural, viewport]);\n\n // Report what the fit became, so the chrome can show a percentage and the\n // reader's next \"zoom in\" steps from what is on screen.\n useEffect(() => {\n onZoomResolved?.(scale);\n }, [scale, onZoomResolved]);\n\n const estimateSize = useCallback(\n (index: number) =>\n Math.round((naturalAt(index)?.height ?? FALLBACK_PAGE_HEIGHT) * scale) +\n PAGE_BORDER +\n PAGE_GAP,\n [naturalAt, scale],\n );\n\n const virtualizer = usePagedScroll({\n count: pdf.pageCount,\n listRef,\n pageNumber,\n goToPage,\n estimateSize,\n sizeKey: scale,\n });\n\n // LOCATE happened in the shell; the per-page views below do MAP and PAINT.\n // Computed once here because it is a function of the whole projection, not of\n // any one page.\n const marks = useMemo(() => toMarkRanges(highlights, pdf.text?.length ?? 0), [highlights, pdf]);\n\n // Scrolling to the cited page is the first half of \"take me there\"; the mark's\n // own `scrollIntoView`, inside the page view, is the second. Keyed on the page\n // NUMBER, so a reader who scrolls away while the same citation stays active is\n // not dragged back.\n const activePage = pageOfHighlight(\n highlights?.find((highlight) => highlight.id === activeHighlightId),\n pdf.textIndex,\n );\n useEffect(() => {\n if (activePage === undefined || activePage < 1 || activePage > pdf.pageCount) return;\n goToPage(activePage);\n }, [activePage, pdf.pageCount, goToPage]);\n\n return (\n // No viewport of its own: `FileViewerContent` is the one scroll boundary\n // (`viewer-components.md`), and this column is a child of it. The height is\n // the whole document's, so the scrollbar is honest from the first frame\n // rather than growing as pages arrive.\n <div\n ref={listRef}\n data-slot=\"pdf-pages\"\n className={cn(\"relative w-full\", className)}\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((item) => (\n <div\n key={item.key}\n data-index={item.index}\n ref={virtualizer.measureElement}\n className=\"absolute inset-x-0 top-0\"\n style={{\n paddingBottom: PAGE_GAP,\n transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,\n }}\n >\n <PdfPageView\n handle={pdf.handle}\n pageNumber={item.index + 1}\n natural={naturalAt(item.index)}\n scale={scale}\n alt={source.alt}\n highlights={highlights}\n activeHighlightId={activeHighlightId}\n marks={marks}\n pageStart={pdf.textIndex?.spans.find((span) => span.ref === item.index + 1)?.start}\n />\n </div>\n ))}\n </div>\n );\n}\n\ninterface PdfPageViewProps {\n handle: PdfDocument;\n /** 1-based, as everything on the wire is. */\n pageNumber: number;\n /** This page at scale 1, when the adapter measured it. Reserves the box. */\n natural?: PageSize;\n scale: number;\n alt?: string;\n highlights?: readonly ResolvedHighlight[];\n activeHighlightId?: string | null;\n marks: MarkRanges;\n /** Where this page's text begins in the projection, if it was extracted. */\n pageStart?: number;\n}\n\n/**\n * One page: the raster, the transparent text over it, and any citation boxes.\n *\n * A component per page rather than one effect over the visible range, so that\n * mounting and unmounting IS the lifecycle — a page scrolled out of the window\n * cancels its own render task and releases its own pdf.js page, with no\n * bookkeeping to get wrong.\n */\nfunction PdfPageView({\n handle,\n pageNumber,\n natural,\n scale,\n alt,\n highlights,\n activeHighlightId,\n marks,\n pageStart,\n}: PdfPageViewProps) {\n const { t, formatNumber } = useLocale();\n\n const [size, setSize] = useState<{ width: number; height: number } | undefined>(\n natural && { width: natural.width * scale, height: natural.height * scale },\n );\n const [spans, setSpans] = useState<PdfTextSpan[]>([]);\n const [items, setItems] = useState<PdfTextItem[]>([]);\n const [failed, setFailed] = useState<string>();\n\n const pageRef = useRef<HTMLDivElement>(null);\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n useEffect(() => {\n let cancelled = false;\n let task: ReturnType<PdfPage[\"render\"]> | undefined;\n let page: PdfPage | undefined;\n\n void (async () => {\n try {\n page = await handle.getPage(pageNumber);\n if (cancelled) return;\n\n // Rasterize at the device pixel ratio, then present at CSS size — a 1:1\n // canvas is visibly soft on every retina display.\n const ratio = typeof window === \"undefined\" ? 1 : (window.devicePixelRatio ?? 1);\n const viewport = page.getViewport({ scale: scale * ratio });\n const cssViewport = page.getViewport({ scale });\n const canvas = canvasRef.current;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) return;\n\n canvas.width = Math.floor(viewport.width);\n canvas.height = Math.floor(viewport.height);\n setSize({ width: cssViewport.width, height: cssViewport.height });\n\n task = page.render({ canvasContext: context, viewport });\n await task.promise;\n if (cancelled) return;\n\n const content = await page.getTextContent();\n if (cancelled) return;\n setItems(content.items);\n setSpans(toTextSpans(content.items, cssViewport.height, scale));\n setFailed(undefined);\n } catch (error) {\n // A cancelled render is the normal outcome of scrolling quickly; only a\n // settled failure is news (loading-states.md).\n if (cancelled) return;\n const message = error instanceof Error ? error.message : String(error);\n if (/cancel/i.test(message)) return;\n setFailed(message);\n }\n })();\n\n return () => {\n cancelled = true;\n task?.cancel();\n page?.cleanup();\n };\n }, [handle, pageNumber, scale]);\n\n // MAP + PAINT. Turning the shell's offsets into rectangles needs this page's\n // own items, which the effect above already fetched for the text layer.\n const boxes = useMemo<PdfHighlightBox[]>(() => {\n if (!size) return [];\n // A `rect` address is already geometry — it only has to be on this page.\n // `page` defaults to 1 so a single-page citation needs no page number.\n const geometry = (highlights ?? []).filter(\n (highlight) =>\n highlight.status === \"resolved\" &&\n highlight.rects !== undefined &&\n (highlight.page ?? 1) === pageNumber,\n );\n return [\n ...(pageStart === undefined ? [] : rangeBoxes(spans, pageTextChunk(items), pageStart, marks)),\n ...rectBoxes(geometry, size),\n ];\n }, [size, highlights, pageNumber, pageStart, spans, items, marks]);\n\n // The boxes arrive a tick after the page does (the text content is fetched\n // asynchronously), so the scroll waits for them rather than for the id.\n useScrollActiveHighlightIntoView(pageRef, activeHighlightId, boxes);\n\n if (failed) {\n return (\n <div\n className=\"mx-auto flex w-fit items-center justify-center\"\n style={size ? { width: size.width, height: size.height } : undefined}\n >\n <StatePanel\n kind=\"error\"\n title={t(\"viewer.error.parseFailed\")}\n description={t(\"viewer.pdf.pageFailed\", {\n page: formatNumber(pageNumber),\n })}\n />\n </div>\n );\n }\n\n return (\n <div\n ref={pageRef}\n data-slot=\"pdf-page\"\n data-page={pageNumber}\n className=\"border-border relative mx-auto w-fit border shadow-sm\"\n style={size ? { width: size.width, height: size.height } : undefined}\n >\n <canvas\n ref={canvasRef}\n // The page's own description, so a screen reader gets what the document\n // is rather than \"canvas\". The text layer below carries the words\n // themselves. Every page names its own number, which is what makes a\n // continuous column navigable by heading-less landmark browsing.\n role=\"img\"\n aria-label={alt ?? t(\"viewer.pdf.page\", { page: formatNumber(pageNumber) })}\n className=\"block\"\n style={size ? { width: size.width, height: size.height } : undefined}\n />\n {/* Transparent, selectable text over the raster. `select-text` and the\n transforms come from pdf.js's own item geometry. */}\n <div aria-hidden={spans.length === 0} className=\"absolute inset-0 select-text\">\n {spans.map((span, index) => (\n <span\n key={index}\n className=\"absolute origin-top-left whitespace-pre text-transparent\"\n style={{\n left: span.left,\n top: span.top,\n fontSize: span.fontSize,\n lineHeight: 1,\n }}\n >\n {span.text}\n </span>\n ))}\n </div>\n {/* Decorative: the words themselves are in the text layer above, and the\n shell's status line is what names the citation. A plate here would\n otherwise be read out as an unlabelled region.\n\n Translucent rather than a solid plate, because the \"ink\" it sits on is\n the raster below it — an opaque mark would delete the very sentence it\n is pointing at. The current box is not distinguished by colour alone\n (WCAG 1.4.1): it is also drawn twice as thick. */}\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n {boxes.map((box, index) => (\n <div\n key={index}\n data-slot=\"highlight-rect\"\n // Presence form, matching every other painter's `data-active`.\n data-active={box.active ? \"\" : undefined}\n className={cn(\n \"absolute rounded-xs\",\n box.active\n ? \"bg-highlight-active/30 border-highlight-active border-2\"\n : \"bg-highlight/35 border-highlight border\",\n )}\n style={{\n left: box.left,\n top: box.top,\n width: box.width,\n height: box.height,\n }}\n />\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * pdf.js text geometry → CSS boxes.\n *\n * Item transforms are `[a, b, c, d, e, f]` in PDF user space, whose origin is\n * the BOTTOM-left of the page; CSS measures from the top. `d` carries the glyph\n * height, which is the font size once scaled.\n *\n * Whitespace-only items are dropped — they would be invisible clutter in the\n * text layer — so each span carries the index of the item it came from. That is\n * what lets a highlight box find its geometry after the filtering, without\n * either side counting positions.\n */\nexport function toTextSpans(\n items: { str: string; transform: number[]; width?: number }[],\n pageHeight: number,\n scale: number,\n): PdfTextSpan[] {\n const spans: PdfTextSpan[] = [];\n items.forEach((item, index) => {\n if (!item.str.trim()) return;\n const [, , , d = 0, e = 0, f = 0] = item.transform;\n const fontSize = Math.abs(d) * scale;\n spans.push({\n text: item.str,\n left: e * scale,\n top: pageHeight - f * scale - fontSize,\n width: (item.width ?? 0) * scale,\n fontSize,\n item: index,\n });\n });\n return spans;\n}\n\nconst adapterModule: AdapterModule = {\n manifest: pdfManifest,\n create: () => new PdfAdapter(),\n Renderer: PdfRenderer,\n};\n\nexport default adapterModule;\n","/**\n * PDF text ↔ page geometry — the MAP half of the highlight funnel for a raster.\n *\n * A PDF page is pixels. There is no DOM text to wrap in a `<mark>`, so the paint\n * step here is a layer of BOXES over the canvas, and this module is what turns\n * \"characters 812–847 of the projection\" into those boxes.\n *\n * ## Why the projection is rebuilt rather than remembered\n *\n * `AdapterDocument.text` is assembled at LOAD time, one page at a time; the\n * renderer re-fetches the current page's text content anyway (it needs the same\n * items to position the selectable text layer). So both sides call\n * {@link pageTextChunk}, and the projection and the geometry agree by\n * construction instead of by two hand-kept copies of the same join rule.\n *\n * ## Why the boxes are proportional, and what that costs\n *\n * pdf.js reports one box per text ITEM — a run of glyphs — and no per-character\n * geometry. When a range covers only part of an item, the box is cut\n * proportionally by character count. That is a linear approximation of a\n * proportional font, so it can be off by a glyph or two inside a long run; PDF\n * items are usually a word or a line fragment, where the error is invisible.\n *\n * The alternative — DOM ranges over the transparent text layer — would be WORSE\n * here, not better: that layer renders in a generic font at pdf.js's transform,\n * so its glyph advances do not match the raster underneath it at all. The item\n * box comes from the file itself and does.\n */\n\nimport type { DocumentRect } from \"@elabs-ai/components-ui\";\n\nimport type { ResolvedHighlight } from \"../../core/highlight\";\nimport { localizeRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { spanAt, type TextIndex } from \"../../core/text-index\";\n\n/** Written between two text items of the same page. */\nexport const PDF_ITEM_SEPARATOR = \" \";\n\n/** Written between two pages of the projection. */\nexport const PDF_PAGE_SEPARATOR = \"\\n\\n\";\n\n/** The part of a pdf.js text item this module needs. */\nexport interface PdfTextItemLike {\n str: string;\n}\n\n/** Half-open `[start, end)` into the page's own chunk, one per item. */\nexport interface PdfItemSpan {\n start: number;\n end: number;\n}\n\n/** One page's contribution to the projection, plus where each item landed in it. */\nexport interface PdfPageChunk {\n text: string;\n /** Index-aligned with the items passed in — including the empty ones. */\n spans: readonly PdfItemSpan[];\n}\n\n/**\n * Join a page's text items the way the projection does, and record each item's\n * offsets.\n *\n * Empty items keep their place: they still consume a separator, so an item's\n * index into this array is the same index pdf.js handed back. Dropping them\n * would shift every following offset by one and put every mark a character off.\n */\nexport function pageTextChunk(items: readonly PdfTextItemLike[]): PdfPageChunk {\n const spans: PdfItemSpan[] = [];\n const parts: string[] = [];\n let cursor = 0;\n\n items.forEach((item, index) => {\n if (index > 0) cursor += PDF_ITEM_SEPARATOR.length;\n parts.push(item.str);\n spans.push({ start: cursor, end: cursor + item.str.length });\n cursor += item.str.length;\n });\n\n return { text: parts.join(PDF_ITEM_SEPARATOR), spans };\n}\n\n/** A positioned text item, in CSS pixels relative to the rendered page. */\nexport interface PdfTextSpan {\n text: string;\n left: number;\n top: number;\n /** The item's own advance width. `0` when pdf.js did not report one. */\n width: number;\n fontSize: number;\n /** Index into the page's original items — the seam back to the projection. */\n item: number;\n}\n\n/** A rectangle to paint over the page, in CSS pixels. */\nexport interface PdfHighlightBox {\n left: number;\n top: number;\n width: number;\n height: number;\n /** Whether this box is the highlight the viewer is currently pointed at. */\n active: boolean;\n}\n\n/**\n * Fallback advance per character when pdf.js reports no width.\n *\n * Half an em is the rough mean advance of Latin text; it only ever decides how\n * wide a box is drawn, never where the reader is taken.\n */\nconst FALLBACK_ADVANCE = 0.5;\n\n/**\n * Boxes for every marked range that falls on this page.\n *\n * `pageStart` is where this page's chunk begins in the projection, which is what\n * makes a whole-document offset comparable with a per-page item offset.\n */\nexport function rangeBoxes(\n spans: readonly PdfTextSpan[],\n chunk: PdfPageChunk,\n pageStart: number,\n marks: MarkRanges,\n): PdfHighlightBox[] {\n if (marks.ranges.length === 0) return [];\n\n const boxes: PdfHighlightBox[] = [];\n for (const span of spans) {\n const item = chunk.spans[span.item];\n if (!item || item.end === item.start) continue;\n\n const local = localizeRanges(marks, pageStart + item.start, pageStart + item.end);\n if (local.ranges.length === 0) continue;\n\n const length = item.end - item.start;\n const advance = span.width > 0 ? span.width / length : span.fontSize * FALLBACK_ADVANCE;\n local.ranges.forEach(([from, to], index) => {\n boxes.push({\n left: span.left + advance * from,\n top: span.top,\n width: advance * (to - from),\n height: span.fontSize,\n active: index === local.activeIndex,\n });\n });\n }\n return boxes;\n}\n\n/**\n * Boxes for `rect` addresses, whose geometry is already the answer.\n *\n * Fractions rather than pixels is what makes them survive zoom and a resize: the\n * same address is correct at 50% and at 300% because it is multiplied by the\n * page's CURRENT rendered size every time.\n */\nexport function rectBoxes(\n highlights: readonly ResolvedHighlight[],\n size: { width: number; height: number },\n): PdfHighlightBox[] {\n const boxes: PdfHighlightBox[] = [];\n for (const highlight of highlights) {\n for (const rect of highlight.rects ?? []) {\n boxes.push({\n left: clampFraction(rect.x) * size.width,\n top: clampFraction(rect.y) * size.height,\n width: clampFraction(rect.width) * size.width,\n height: clampFraction(rect.height) * size.height,\n active: highlight.active,\n });\n }\n }\n return boxes;\n}\n\n/** Keep a foreign document's geometry inside the page it claims to be on. */\nfunction clampFraction(value: DocumentRect[\"x\"]): number {\n if (!Number.isFinite(value)) return 0;\n return Math.max(0, Math.min(1, value));\n}\n\n/**\n * Which page a highlight is on, so activating it can turn to that page.\n *\n * A `rect` carries its own page — geometry has no range to look up. Everything\n * else is looked up through the document's own index, because the ref type is the\n * adapter's business and this adapter's is the page number. A caller's page HINT\n * is deliberately not consulted: it is the resolver's job to leave it off\n * (`ResolvedHighlight.page`), since a stale hint would turn to a page the mark is\n * not on. A range landing on a page separator, or past the extraction cap, has no\n * page and the pager stays put rather than jumping somewhere arbitrary.\n */\nexport function pageOfHighlight(\n highlight: ResolvedHighlight | undefined,\n index: TextIndex<unknown> | undefined,\n): number | undefined {\n if (!highlight || highlight.status !== \"resolved\") return undefined;\n if (highlight.page !== undefined) return highlight.page;\n if (!highlight.range || !index) return undefined;\n const span = spanAt(index, highlight.range[0]);\n return typeof span?.ref === \"number\" ? span.ref : undefined;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/pptx/pptx-adapter.tsx","../src/adapters/pptx/pptx-model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * PowerPoint adapter — a deck becomes a slide outline you can page through.\n *\n * A `.pptx` is a zip of XML, so there is no PowerPoint library here: jszip opens\n * the container and the platform's own `DOMParser` reads the parts. What is\n * extracted, and why it is an outline rather than a reproduction of the slide\n * canvas, is argued in `pptx-model.ts`.\n *\n * The deck is one continuous column of slides, virtualized over the shell's\n * viewport — the same treatment the PDF pages get, for the same reason: reading\n * a deck end to end is scrolling, not clicking \"next\" 60 times. The slide number\n * follows the scroll, and the pager scrolls.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { cn, ProseHeading, useLocale } from \"@elabs-ai/components-ui\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\n\nimport { MarkedText } from \"../../components/marked-text\";\nimport { ViewerError, toViewerError } from \"../../core/errors\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { spanAt, type TextIndex } from \"../../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport { usePageControl } from \"../../core/use-page-control\";\nimport { usePagedScroll } from \"../../core/use-paged-scroll\";\nimport { useViewportSize } from \"../../core/use-viewport-size\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { pptxManifest } from \"./pptx-manifest\";\nimport {\n notesTarget,\n orderSlidePaths,\n parseNotes,\n parseSlide,\n slidesToTextWithMap,\n PPTX_NOTES_LINE,\n PPTX_TITLE_LINE,\n type PptxRef,\n type PptxSlide,\n} from \"./pptx-model\";\n\nexport interface PptxDocument extends AdapterDocument {\n kind: \"pptx\";\n slides: PptxSlide[];\n pageCount: number;\n /** Which slide and line each stretch of `text` came from. */\n textIndex?: TextIndex<PptxRef>;\n}\n\n/** Indent per outline level. Four rungs is as deep as a readable slide ever goes. */\nconst LEVEL_INDENT = [\"ps-0\", \"ps-4\", \"ps-8\", \"ps-12\"] as const;\n\n/** The slide column's own cap, matching the `max-w-3xl` on the frame. */\nconst SLIDE_MAX_WIDTH = 768;\n\n/** Space between two slides in the column, in CSS pixels. */\nconst SLIDE_GAP = 16;\n\n/** Room reserved for a slide's speaker notes before they are measured. */\nconst NOTES_ESTIMATE = 72;\n\nclass PptxAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<PptxDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const JSZip = (await import(\"jszip\")).default;\n\n try {\n const zip = await JSZip.loadAsync(buffer);\n const parser = new DOMParser();\n const parse = (xml: string) => parser.parseFromString(xml, \"application/xml\");\n\n const slidePaths = orderSlidePaths(Object.keys(zip.files));\n if (slidePaths.length === 0) {\n throw new ViewerError(\"parse-failed\", \"The presentation contains no slides.\", {\n fileName: source.name,\n });\n }\n\n const slides: PptxSlide[] = [];\n for (const [position, path] of slidePaths.entries()) {\n if (context.signal?.aborted) break;\n const xml = await zip.file(path)?.async(\"string\");\n if (!xml) continue;\n const slide = parseSlide(parse(xml), position + 1);\n\n // Notes are best-effort: a deck with no notes part, or one whose\n // relationships do not resolve, is a complete deck — not a failure.\n const relsXml = await zip\n .file(`${dirOf(path)}/_rels/${baseOf(path)}.rels`)\n ?.async(\"string\");\n const notesPath = relsXml ? notesTarget(parse(relsXml), path) : undefined;\n const notesXml = notesPath ? await zip.file(notesPath)?.async(\"string\") : undefined;\n if (notesXml) slide.notes = parseNotes(parse(notesXml));\n\n slides.push(slide);\n }\n\n const textIndex = slidesToTextWithMap(slides);\n return {\n kind: \"pptx\",\n slides,\n pageCount: slides.length,\n text: textIndex.text,\n textIndex,\n };\n } catch (error) {\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n}\n\nfunction dirOf(path: string): string {\n return path.slice(0, path.lastIndexOf(\"/\"));\n}\n\nfunction baseOf(path: string): string {\n return path.slice(path.lastIndexOf(\"/\") + 1);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\nfunction PptxRenderer({\n document: doc,\n className,\n baseHeadingLevel = 2,\n highlights,\n activeHighlightId,\n pageNumber,\n onPageChange,\n}: AdapterRendererProps) {\n const deck = doc as PptxDocument;\n // One pager for every paginated format, so a deck's slides and a PDF's pages\n // are driven by the same shell control (ADR 0026). 1-based on the wire; this\n // renderer's array is 0-based, and the conversion stays local.\n const [slideNumber, goToSlide] = usePageControl(pageNumber, onPageChange, deck.slides.length);\n const listRef = useRef<HTMLDivElement>(null);\n const viewport = useViewportSize(listRef);\n\n const marks = useMemo(\n () => toMarkRanges(highlights, deck.text?.length ?? 0),\n [highlights, deck.text],\n );\n\n // A slide frame is 16:9 inside a capped column, so its height follows from the\n // pane's width — no measurement of the slide itself is needed for the first\n // frame. `measureElement` corrects a slide whose outline overflows the ratio,\n // and whose notes are longer than the estimate.\n const frameWidth = Math.min(SLIDE_MAX_WIDTH, (viewport?.width ?? SLIDE_MAX_WIDTH) - SLIDE_GAP);\n const estimateSize = useCallback(\n (index: number) =>\n Math.round((frameWidth * 9) / 16) +\n SLIDE_GAP +\n (deck.slides[index]?.notes ? NOTES_ESTIMATE : 0),\n [frameWidth, deck.slides],\n );\n\n const virtualizer = usePagedScroll({\n count: deck.slides.length,\n listRef,\n pageNumber: slideNumber,\n goToPage: goToSlide,\n estimateSize,\n sizeKey: frameWidth,\n });\n\n // Where each line of the deck begins in the projection, keyed by slide. Built\n // once for the whole deck rather than per slide: several slides are on screen\n // at a time now, and one pass over the spans is cheaper than one pass per\n // visible slide on every scroll.\n const starts = useMemo(() => {\n const map = new Map<number, Map<number, number>>();\n for (const span of deck.textIndex?.spans ?? []) {\n const slide = map.get(span.ref.slide) ?? new Map<number, number>();\n slide.set(span.ref.line, span.start);\n map.set(span.ref.slide, slide);\n }\n return map;\n }, [deck.textIndex]);\n\n // Scrolling to the cited slide is this format's half of \"take me there\". Keyed\n // on the slide NUMBER, so a reader who scrolls away while the same citation is\n // still active is not dragged back (the same trade the PDF column makes).\n const activeSlide = useMemo(() => {\n const active = highlights?.find((highlight) => highlight.id === activeHighlightId);\n if (!active || active.status !== \"resolved\" || !active.range || !deck.textIndex) {\n return undefined;\n }\n return spanAt(deck.textIndex, active.range[0])?.ref.slide;\n }, [highlights, activeHighlightId, deck.textIndex]);\n useEffect(() => {\n if (activeSlide === undefined) return;\n const position = deck.slides.findIndex((candidate) => candidate.index === activeSlide);\n if (position >= 0) goToSlide(position + 1);\n }, [activeSlide, deck.slides, goToSlide]);\n\n // The mark only exists once the cited slide is mounted, so the scroll waits\n // for the slide as well as for the id.\n useScrollActiveHighlightIntoView(listRef, activeHighlightId, slideNumber);\n\n return (\n // No viewport of its own: the slide chrome moved to the shell (ADR 0026), so\n // there is no fixed control above a scrolling body and `FileViewerContent`\n // is the one scroll boundary (`viewer-components.md`).\n <div\n ref={listRef}\n data-slot=\"pptx-slides\"\n className={cn(\"relative w-full\", className)}\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((item) => {\n const slide = deck.slides[item.index];\n if (!slide) return null;\n return (\n <div\n key={item.key}\n data-index={item.index}\n ref={virtualizer.measureElement}\n className=\"absolute inset-x-0 top-0\"\n style={{\n paddingBottom: SLIDE_GAP,\n transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,\n }}\n >\n <PptxSlideView\n slide={slide}\n slideNumber={item.index + 1}\n baseHeadingLevel={baseHeadingLevel}\n marks={marks}\n starts={starts.get(slide.index)}\n />\n </div>\n );\n })}\n </div>\n );\n}\n\ninterface PptxSlideViewProps {\n slide: PptxSlide;\n /** 1-based position in the deck, which is what the reader is shown. */\n slideNumber: number;\n baseHeadingLevel: ProseHeadingLevel;\n marks: MarkRanges;\n /** Where each of this slide's lines begins in the projection. */\n starts?: Map<number, number>;\n}\n\n/** One slide: the 16:9 frame, its outline, and any speaker notes beneath it. */\nfunction PptxSlideView({\n slide,\n slideNumber,\n baseHeadingLevel,\n marks,\n starts,\n}: PptxSlideViewProps) {\n const { t, formatNumber } = useLocale();\n const isEmpty = !slide.title && slide.lines.length === 0;\n\n return (\n <>\n {/* The slide surface: `bg-card` above the pane's ground, `aspect-video`\n so a deck still reads as a deck — this is the shape of the thing,\n which is the one part of the layout an outline can honestly keep. */}\n {/* A slide that overflows its 16:9 frame scrolls, and a scrollable\n region with no focusable content cannot be reached by keyboard\n (WCAG 2.1.1). `tabIndex={0}` makes the slide itself the stop that\n arrow keys and Page Up/Down drive. This is the one nested scroller the\n rule allows: a fixed-ratio frame whose content may not fit it. */}\n <section\n tabIndex={0}\n data-slot=\"pptx-slide\"\n data-page={slideNumber}\n aria-label={t(\"viewer.pptx.slide\", {\n slide: formatNumber(slideNumber),\n })}\n className=\"border-border bg-card focus-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm\"\n >\n {/* A slide title is the deck's top rung, so it sits at the host's base.\n The stand-in for an untitled slide is OUR text, not the deck's, so\n it is not in the projection and cannot be cited. */}\n <ProseHeading level={baseHeadingLevel}>\n {slide.title === undefined ? (\n t(\"viewer.pptx.untitled\")\n ) : (\n <MarkedText text={slide.title} marks={marks} start={starts?.get(PPTX_TITLE_LINE)} />\n )}\n </ProseHeading>\n {isEmpty ? (\n <p className=\"text-muted-foreground text-body\">{t(\"viewer.pptx.empty\")}</p>\n ) : (\n <ul className=\"text-body space-y-1.5\">\n {slide.lines.map((line, lineIndex) => (\n <li\n key={lineIndex}\n className={cn(\"whitespace-pre-wrap\", LEVEL_INDENT[Math.min(line.level, 3)])}\n >\n <MarkedText text={line.text} marks={marks} start={starts?.get(lineIndex)} />\n </li>\n ))}\n </ul>\n )}\n </section>\n\n {slide.notes && (\n <section\n aria-label={t(\"viewer.pptx.notes\")}\n className=\"mx-auto mt-3 w-full max-w-3xl space-y-1\"\n >\n <h3 className=\"text-meta text-muted-foreground\">{t(\"viewer.pptx.notes\")}</h3>\n <p className=\"text-body text-muted-foreground whitespace-pre-wrap\">\n <MarkedText text={slide.notes} marks={marks} start={starts?.get(PPTX_NOTES_LINE)} />\n </p>\n </section>\n )}\n </>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: pptxManifest,\n create: () => new PptxAdapter(),\n Renderer: PptxRenderer,\n};\n\nexport default adapterModule;\n","/**\n * PowerPoint XML → a slide outline.\n *\n * ## What a deck preview honestly is\n *\n * A `.pptx` slide is absolutely-positioned drawing: shapes at EMU coordinates,\n * theme-driven fills, transitions, embedded media. Reproducing that faithfully\n * is a rendering engine, not a preview — and a half-faithful reproduction is\n * worse than none, because it looks like the deck while quietly lying about it.\n *\n * So this reads the deck as an OUTLINE: per slide, the title, the text in\n * document order (with its indent level), and the speaker notes. That is the\n * content a reader is looking for when they open a deck in a file browser, it\n * renders in this system's typography, and it is honest about what it is.\n * Anything positional — layout, images, charts, transitions — is deliberately\n * absent; the toolbar's download is the answer for the real thing.\n *\n * Parsing goes through namespace URIs rather than `a:t` / `p:sp` tag names,\n * because the prefix is only a convention: a generator is free to bind the same\n * namespace to a different prefix, and tag-name matching would silently return\n * an empty deck.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../../core/text-index\";\n\n/** The DrawingML namespace — text, paragraphs, tables. */\nexport const DRAWING_NS = \"http://schemas.openxmlformats.org/drawingml/2006/main\";\n/** The PresentationML namespace — shapes, placeholders, the shape tree. */\nexport const PRESENTATION_NS = \"http://schemas.openxmlformats.org/presentationml/2006/main\";\n\n/** One line of slide text, with the indent level PowerPoint gave it. */\nexport interface PptxLine {\n text: string;\n /** Outline depth, `0` for a top-level bullet. */\n level: number;\n}\n\nexport interface PptxSlide {\n /** 1-based position in the deck. */\n index: number;\n /** The title placeholder's text, when the slide has one. */\n title?: string;\n /** Every other line, in document order. */\n lines: PptxLine[];\n /** The slide's speaker notes, when it has any. */\n notes?: string;\n}\n\n/**\n * Order slide parts the way the deck does.\n *\n * A zip lists its entries in whatever order they were written, and\n * `slide10.xml` sorts before `slide2.xml` as a string — so the number is pulled\n * out and compared as a number.\n */\nexport function orderSlidePaths(paths: string[]): string[] {\n return paths\n .filter((path) => /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path))\n .sort((left, right) => slideNumber(left) - slideNumber(right));\n}\n\n/** The `N` in `…/slideN.xml`, or `0` when there is none. */\nexport function slideNumber(path: string): number {\n const match = /(\\d+)\\.xml$/.exec(path);\n return match?.[1] ? Number(match[1]) : 0;\n}\n\n/** All text under a node, in document order, as one string. */\nfunction textOf(node: Element): string {\n const runs = node.getElementsByTagNameNS(DRAWING_NS, \"t\");\n let text = \"\";\n for (const run of Array.from(runs)) text += run.textContent ?? \"\";\n return text.trim();\n}\n\n/** The paragraphs directly under a shape, as lines. Empty paragraphs are dropped. */\nfunction linesOf(shape: Element): PptxLine[] {\n const lines: PptxLine[] = [];\n for (const paragraph of Array.from(shape.getElementsByTagNameNS(DRAWING_NS, \"p\"))) {\n const text = textOf(paragraph);\n if (!text) continue;\n // `lvl` is absent for a top-level bullet, which is the common case.\n const properties = paragraph.getElementsByTagNameNS(DRAWING_NS, \"pPr\")[0];\n const level = Number(properties?.getAttribute(\"lvl\") ?? 0);\n lines.push({ text, level: Number.isFinite(level) ? level : 0 });\n }\n return lines;\n}\n\n/** The placeholder type a shape declares (`title`, `ctrTitle`, `body`, …), if any. */\nfunction placeholderType(shape: Element): string | undefined {\n const placeholder = shape.getElementsByTagNameNS(PRESENTATION_NS, \"ph\")[0];\n return placeholder?.getAttribute(\"type\") ?? undefined;\n}\n\n/** Table text, row by row, tab-separated — a table on a slide is usually the point of it. */\nfunction tableLines(frame: Element): PptxLine[] {\n const lines: PptxLine[] = [];\n for (const row of Array.from(frame.getElementsByTagNameNS(DRAWING_NS, \"tr\"))) {\n const cells = Array.from(row.getElementsByTagNameNS(DRAWING_NS, \"tc\")).map(textOf);\n const text = cells.join(\"\\t\").trim();\n if (text) lines.push({ text, level: 0 });\n }\n return lines;\n}\n\n/**\n * Parse one `ppt/slides/slideN.xml` document into a slide.\n *\n * The shape tree is walked in document order, so the outline reads in the order\n * the shapes were authored rather than in the order the file happens to store\n * them. A shape kind this does not name contributes nothing — the same\n * allowlist-by-parse the Word adapter uses.\n */\nexport function parseSlide(document: Document, index: number): PptxSlide {\n const tree = document.getElementsByTagNameNS(PRESENTATION_NS, \"spTree\")[0];\n const slide: PptxSlide = { index, lines: [] };\n if (!tree) return slide;\n\n for (const node of Array.from(tree.children)) {\n if (node.namespaceURI !== PRESENTATION_NS) continue;\n if (node.localName === \"sp\") {\n const type = placeholderType(node);\n const lines = linesOf(node);\n if ((type === \"title\" || type === \"ctrTitle\") && !slide.title) {\n // A title placeholder can hold several paragraphs; they are one heading.\n const title = lines.map((line) => line.text).join(\" \");\n if (title) slide.title = title;\n continue;\n }\n slide.lines.push(...lines);\n continue;\n }\n if (node.localName === \"graphicFrame\") slide.lines.push(...tableLines(node));\n }\n return slide;\n}\n\n/** Parse a `ppt/notesSlides/notesSlideN.xml` document into its notes text. */\nexport function parseNotes(document: Document): string | undefined {\n const tree = document.getElementsByTagNameNS(PRESENTATION_NS, \"spTree\")[0];\n if (!tree) return undefined;\n\n const lines: string[] = [];\n for (const shape of Array.from(tree.getElementsByTagNameNS(PRESENTATION_NS, \"sp\"))) {\n // The notes part repeats the slide's own body as a non-editable copy; only\n // the notes placeholder holds what the presenter actually wrote.\n if (placeholderType(shape) !== \"body\") continue;\n for (const line of linesOf(shape)) lines.push(line.text);\n }\n const notes = lines.join(\"\\n\").trim();\n return notes || undefined;\n}\n\n/** The OPC relationships namespace — how a part points at another part. */\nexport const RELATIONSHIP_NS = \"http://schemas.openxmlformats.org/package/2006/relationships\";\n\n/**\n * Resolve a relationship target (`../notesSlides/notesSlide1.xml`) against the\n * part that declared it (`ppt/slides/slide1.xml`).\n *\n * Zip entry names are plain strings, not URLs, so `..` has to be walked by hand.\n */\nexport function resolveRelative(fromPart: string, target: string): string {\n if (target.startsWith(\"/\")) return target.slice(1);\n const segments = fromPart.split(\"/\").slice(0, -1);\n for (const segment of target.split(\"/\")) {\n if (segment === \".\" || segment === \"\") continue;\n if (segment === \"..\") segments.pop();\n else segments.push(segment);\n }\n return segments.join(\"/\");\n}\n\n/**\n * The notes part a slide points at, if any.\n *\n * Read from the slide's own `_rels`, not by matching `slideN` to `notesSlideN`:\n * the numbers agree in decks PowerPoint wrote from scratch and drift in decks\n * that have had slides deleted, which would attach the wrong presenter notes to\n * the wrong slide — a quiet, plausible-looking error.\n */\nexport function notesTarget(rels: Document, slidePath: string): string | undefined {\n for (const relationship of Array.from(\n rels.getElementsByTagNameNS(RELATIONSHIP_NS, \"Relationship\"),\n )) {\n if (!relationship.getAttribute(\"Type\")?.endsWith(\"/notesSlide\")) continue;\n const target = relationship.getAttribute(\"Target\");\n if (target) return resolveRelative(slidePath, target);\n }\n return undefined;\n}\n\n/** Between two lines of the same slide. */\nexport const PPTX_LINE_SEPARATOR = \"\\n\";\n/** Between two slides. */\nexport const PPTX_SLIDE_SEPARATOR = \"\\n\\n\";\n\n/** The `line` of a slide's title. */\nexport const PPTX_TITLE_LINE = -1;\n/** The `line` of a slide's speaker notes. */\nexport const PPTX_NOTES_LINE = -2;\n\n/** Where a stretch of the projection came from in the outline. */\nexport interface PptxRef {\n /** 1-based slide position, the same number {@link PptxSlide.index} carries. */\n slide: number;\n /** Body-line index, or {@link PPTX_TITLE_LINE} / {@link PPTX_NOTES_LINE}. */\n line: number;\n}\n\n/**\n * Plain-text projection of a deck, plus the map back into the outline.\n *\n * A slide's title, its lines and its notes are each their own chunk, so a\n * citation resolves to the exact line the renderer draws rather than to \"slide\n * 4\". The blank line between slides is written before whichever chunk turns out\n * to be the next slide's first — a slide with no text at all contributes\n * nothing, instead of a run of empty lines that would shift every offset after\n * it.\n */\nexport function slidesToTextWithMap(slides: PptxSlide[]): TextIndex<PptxRef> {\n const builder = createTextIndexBuilder<PptxRef>({ separator: PPTX_LINE_SEPARATOR });\n slides.forEach((slide, index) => {\n let separator = index === 0 ? undefined : PPTX_SLIDE_SEPARATOR;\n const push = (chunk: string | undefined, line: number) => {\n if (!chunk) return;\n builder.push(chunk, { slide: slide.index, line }, separator);\n separator = undefined;\n };\n\n push(slide.title, PPTX_TITLE_LINE);\n slide.lines.forEach((line, lineIndex) => {\n push(line.text, lineIndex);\n });\n push(slide.notes, PPTX_NOTES_LINE);\n });\n return builder.build();\n}\n\n/**\n * Plain-text projection of a deck — powers search, copy and the raw view. A thin\n * wrapper so the projection has exactly one definition: it is the index's own\n * text, never a second assembly that could drift from it.\n */\nexport function slidesToText(slides: PptxSlide[]): string {\n return slidesToTextWithMap(slides).text;\n}\n"],"mappings":"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","names":["slideNumber"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/text/text-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Plain-text adapter — the fallback for anything readable as UTF-8.\n *\n * Deliberately does NOT do SYNTAX highlighting — that is the separate `code`\n * adapter, registered at a higher priority for code extensions. Keeping them\n * apart means a 40 MB log file never drags a tokenizer into the bundle, and a\n * consumer can drop the tokenizer without losing plain text. (Citation and\n * find-in-document marks are a different thing entirely and DO render here.)\n *\n * Large files are truncated at load, not at render: putting ten million\n * characters into one `<pre>` locks the main thread with no way back. The cut is\n * reported so the UI can say so rather than silently lying about the content.\n */\n\nimport { useMemo, useRef } from \"react\";\n\nimport { MatchHighlight, cn, useLocale } from \"@elabs-ai/components-ui\";\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\n\nimport { toViewerError } from \"../../core/errors\";\nimport { toMarkRanges } from \"../../core/highlight-marks\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { textManifest } from \"./text-manifest\";\n\n/**\n * Characters kept before truncation. ~2M is comfortably past any file a person\n * reads in a preview pane and still renders in one frame.\n */\nexport const TEXT_CHARACTER_LIMIT = 2_000_000;\n\nexport interface TextDocument extends AdapterDocument {\n kind: \"text\";\n text: string;\n /** Total characters in the file, when more than what is shown. */\n totalCharacters?: number;\n}\n\nclass TextAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<TextDocument> {\n let raw: string;\n try {\n raw = await source.text(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n if (raw.length <= TEXT_CHARACTER_LIMIT) return { kind: \"text\", text: raw };\n return {\n kind: \"text\",\n text: raw.slice(0, TEXT_CHARACTER_LIMIT),\n totalCharacters: raw.length,\n // So a passage the shell cannot find is reported as \"past the part we\n // previewed\" rather than \"not in this document\" — different news.\n textTruncated: true,\n };\n }\n}\n\nfunction TextRenderer({\n document: doc,\n className,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const text = doc as TextDocument;\n const { t, formatNumber } = useLocale();\n const container = useRef<HTMLDivElement>(null);\n const { ranges, activeIndex } = useMemo(\n () => toMarkRanges(highlights, text.text.length),\n [highlights, text.text.length],\n );\n useScrollActiveHighlightIntoView(container, activeHighlightId);\n\n return (\n // No `overflow-auto` here: `FileViewerContent` is the scroll boundary, and\n // nesting a second one clips the text above the outer pane's own padding.\n <div ref={container} className={cn(\"flex flex-col gap-2\", className)}>\n {text.totalCharacters !== undefined && (\n // Not an error — the file is fine, we are showing part of it. A status\n // region, so AT hears it once rather than on every scroll.\n <p role=\"status\" className=\"text-meta text-muted-foreground\">\n {t(\"viewer.text.truncated\", {\n shown: formatNumber(text.text.length),\n total: formatNumber(text.totalCharacters),\n })}\n </p>\n )}\n <pre className=\"text-code font-mono break-words whitespace-pre-wrap\">\n {ranges.length === 0 ? (\n text.text\n ) : (\n // `MatchHighlight` renders the WHOLE string with the matches wrapped,\n // so nothing is lost when a highlight is present — a screen reader\n // still reads the file continuously.\n <MatchHighlight text={text.text} ranges={ranges} activeIndex={activeIndex} />\n )}\n </pre>\n </div>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: textManifest,\n create: () => new TextAdapter(),\n Renderer: TextRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAS,SAAS,cAAc;AAEhC,SAAS,gBAAgB,IAAI,iBAAiB;AAmE1C,SAII,KAJJ;AAhDG,IAAM,uBAAuB;AASpC,IAAM,cAAN,MAAyC;AAAA,EACvC,MAAM,KAAK,QAA4B,SAAoD;AACzF,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;AAAA,IACxC,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,eAAe,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACrE;AAEA,QAAI,IAAI,UAAU,qBAAsB,QAAO,EAAE,MAAM,QAAQ,MAAM,IAAI;AACzE,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,IAAI,MAAM,GAAG,oBAAoB;AAAA,MACvC,iBAAiB,IAAI;AAAA;AAAA;AAAA,MAGrB,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAEA,SAAS,aAAa;AAAA,EACpB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,OAAO;AACb,QAAM,EAAE,GAAG,aAAa,IAAI,UAAU;AACtC,QAAM,YAAY,OAAuB,IAAI;AAC7C,QAAM,EAAE,QAAQ,YAAY,IAAI;AAAA,IAC9B,MAAM,aAAa,YAAY,KAAK,KAAK,MAAM;AAAA,IAC/C,CAAC,YAAY,KAAK,KAAK,MAAM;AAAA,EAC/B;AACA,mCAAiC,WAAW,iBAAiB;AAE7D;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,KAAK,WAAW,WAAW,GAAG,uBAAuB,SAAS,GAChE;AAAA,WAAK,oBAAoB;AAAA;AAAA,MAGxB,oBAAC,OAAE,MAAK,UAAS,WAAU,mCACxB,YAAE,yBAAyB;AAAA,QAC1B,OAAO,aAAa,KAAK,KAAK,MAAM;AAAA,QACpC,OAAO,aAAa,KAAK,eAAe;AAAA,MAC1C,CAAC,GACH;AAAA,MAEF,oBAAC,SAAI,WAAU,uDACZ,iBAAO,WAAW,IACjB,KAAK;AAAA;AAAA;AAAA;AAAA,QAKL,oBAAC,kBAAe,MAAM,KAAK,MAAM,QAAgB,aAA0B;AAAA,SAE/E;AAAA,OACF;AAAA;AAEJ;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,QAAQ,MAAM,IAAI,YAAY;AAAA,EAC9B,UAAU;AACZ;AAEA,IAAO,uBAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/xlsx/xlsx-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Spreadsheet adapter — a workbook becomes sheets of rows, and rows become a\n * real `Table`.\n *\n * SheetJS is only ever asked for the DATA. Its `sheet_to_html` helper exists and\n * is not used: it returns a markup string with its own inline styling, which is\n * exactly the \"adapter emits HTML\" failure this package was built to avoid\n * (ADR 0024 §3). `sheet_to_json({ header: 1 })` gives raw cell values, which the\n * shared `SheetTable` renders with brand-ui components — so a spreadsheet\n * inherits the theme, the density dial and the keyboard semantics.\n *\n * **`xlsx` is an optional peer with two known advisories.** The npm build\n * (`0.18.5`) carries a prototype-pollution and a ReDoS advisory, both fixed only\n * in SheetJS's self-hosted CDN builds, which are not published to npm. That is a\n * documented, deliberate trade (`docs/CONSUMING.md` §6): a consumer who never\n * opens a spreadsheet never installs it and is never exposed. Treat any workbook\n * opened here as untrusted input.\n */\n\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { cn, Tabs, TabsContent, TabsList, TabsTrigger, useLocale } from \"@elabs-ai/components-ui\";\nimport { useEffect, useMemo, useState } from \"react\";\n\nimport { gridToText, type GridRef } from \"../../components/grid-text\";\nimport { SheetTable } from \"../../components/sheet-table\";\nimport { ViewerError, toViewerError } from \"../../core/errors\";\nimport { toMarkRanges } from \"../../core/highlight-marks\";\nimport { spanAt, type TextIndex } from \"../../core/text-index\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { xlsxManifest } from \"./xlsx-manifest\";\n\n/**\n * Rows rendered per sheet before truncation. Same bound, same reason, as the CSV\n * adapter: a preview pane is for looking, and a real analysis grid is\n * `DataTable` in `@elabs-ai/components-data`.\n */\nexport const XLSX_ROW_LIMIT = 5_000;\n\nexport interface SheetModel {\n /** The tab's own name, as authored in the workbook. */\n name: string;\n /** Header cells, taken from the sheet's first row. */\n columns: string[];\n /** Body rows, already capped at {@link XLSX_ROW_LIMIT}. */\n rows: string[][];\n /** Total body rows in the sheet, when more than what is shown. */\n totalRows?: number;\n}\n\nexport interface XlsxDocument extends AdapterDocument {\n kind: \"xlsx\";\n sheets: SheetModel[];\n /** Which sheet and row each stretch of `text` came from. */\n textIndex?: TextIndex<GridRef>;\n}\n\n/**\n * Does this look like a workbook container at all?\n *\n * SheetJS sniffs its input and will happily read arbitrary text as a one-column\n * CSV — so a damaged `.xlsx` comes back as a sheet holding one junk cell instead\n * of failing. That is worse than an error: it looks like the file opened. Both\n * containers this adapter claims have a fixed signature — OOXML/ODS is a zip\n * (`PK\\x03\\x04`), legacy `.xls` is a CFB compound file — so anything else is\n * rejected before SheetJS gets a chance to be helpful.\n */\nexport function looksLikeWorkbook(bytes: Uint8Array): boolean {\n const zip = [0x50, 0x4b, 0x03, 0x04];\n const cfb = [0xd0, 0xcf, 0x11, 0xe0];\n return [zip, cfb].some((signature) => signature.every((byte, index) => bytes[index] === byte));\n}\n\n/** SheetJS returns whatever the cell held — a date, a number, a formula result. */\nfunction toCellText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n return String(value);\n}\n\nclass XlsxAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<XlsxDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n const bytes = new Uint8Array(buffer);\n if (!looksLikeWorkbook(bytes)) {\n throw new ViewerError(\"parse-failed\", \"The file is not a spreadsheet container.\", {\n fileName: source.name,\n });\n }\n\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const XLSX = await import(\"xlsx\");\n\n try {\n // `cellDates` so a date column reads as a date rather than as Excel's\n // serial day number, which is meaningless to a person looking at it.\n const workbook = XLSX.read(bytes, { type: \"array\", cellDates: true });\n const sheets = workbook.SheetNames.map((name) => {\n const sheet = workbook.Sheets[name];\n const grid: unknown[][] = sheet\n ? XLSX.utils.sheet_to_json(sheet, { header: 1, blankrows: false, defval: \"\" })\n : [];\n const [header = [], ...body] = grid;\n const columns = header.map(toCellText);\n const rows = body.slice(0, XLSX_ROW_LIMIT).map((row) => row.map(toCellText));\n return body.length > XLSX_ROW_LIMIT\n ? { name, columns, rows, totalRows: body.length }\n : { name, columns, rows };\n });\n\n if (sheets.length === 0) {\n throw new ViewerError(\"parse-failed\", \"The workbook contains no sheets.\", {\n fileName: source.name,\n });\n }\n\n // The projection is built by the shared grid builder rather than joined by\n // hand, so the text a citation is addressed against and the map back into\n // the cells cannot drift apart (`core/text-index.ts`).\n const textIndex = gridToText(sheets);\n return {\n kind: \"xlsx\",\n sheets,\n pageCount: sheets.length,\n text: textIndex.text,\n textIndex,\n };\n } catch (error) {\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n}\n\nfunction XlsxRenderer({\n document: doc,\n className,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const workbook = doc as XlsxDocument;\n const { t } = useLocale();\n const [first] = workbook.sheets;\n\n const marks = useMemo(\n () => toMarkRanges(highlights, workbook.text?.length ?? 0),\n [highlights, workbook.text],\n );\n\n // Where each row of each sheet begins in the projection. Built once per\n // document rather than per tab, because switching sheets must not re-walk the\n // spans of every OTHER sheet to answer a question about this one.\n const starts = useMemo(() => {\n const bySheet = new Map<number, Map<number, number>>();\n for (const span of workbook.textIndex?.spans ?? []) {\n let rows = bySheet.get(span.ref.sheet);\n if (!rows) {\n rows = new Map<number, number>();\n bySheet.set(span.ref.sheet, rows);\n }\n rows.set(span.ref.row, span.start);\n }\n return bySheet;\n }, [workbook.textIndex]);\n const rowStartFor = (sheet: number) => (row: number) => starts.get(sheet)?.get(row);\n\n // Turning to the cited SHEET is this format's half of \"take me there\" — the\n // row scroll inside it belongs to `SheetTable`. Keyed on the sheet NAME, so a\n // reader who switches tabs while the same citation stays active is not\n // dragged back (the PDF pager makes the same trade).\n const activeSheet = useMemo(() => {\n const active = highlights?.find((highlight) => highlight.id === activeHighlightId);\n if (!active || active.status !== \"resolved\" || !active.range || !workbook.textIndex) {\n return undefined;\n }\n return spanAt(workbook.textIndex, active.range[0])?.ref.sheet;\n }, [highlights, activeHighlightId, workbook.textIndex]);\n const activeSheetName =\n activeSheet === undefined ? undefined : workbook.sheets[activeSheet]?.name;\n\n const [pinned, setPinned] = useState<string>();\n useEffect(() => {\n if (activeSheetName !== undefined) setPinned(activeSheetName);\n }, [activeSheetName]);\n\n // A one-sheet workbook is the common case, and a tab strip holding a single\n // tab is chrome that decides nothing — so it is not rendered.\n if (!first) return null;\n if (workbook.sheets.length === 1) {\n return (\n <SheetTable\n columns={first.columns}\n rows={first.rows}\n totalRows={first.totalRows}\n className={className}\n marks={marks}\n rowStart={rowStartFor(0)}\n activeHighlightId={activeHighlightId}\n />\n );\n }\n\n return (\n <Tabs\n value={pinned ?? first.name}\n onValueChange={setPinned}\n className={cn(\"flex h-full min-h-0 flex-col gap-2\", className)}\n >\n <TabsList aria-label={t(\"viewer.sheet.tabs\")} className=\"shrink-0 self-start\">\n {workbook.sheets.map((sheet) => (\n <TabsTrigger key={sheet.name} value={sheet.name}>\n {sheet.name}\n </TabsTrigger>\n ))}\n </TabsList>\n {workbook.sheets.map((sheet, index) => (\n <TabsContent key={sheet.name} value={sheet.name} className=\"mt-0 min-h-0 flex-1\">\n <SheetTable\n columns={sheet.columns}\n rows={sheet.rows}\n totalRows={sheet.totalRows}\n marks={marks}\n rowStart={rowStartFor(index)}\n activeHighlightId={activeHighlightId}\n />\n </TabsContent>\n ))}\n </Tabs>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: xlsxManifest,\n create: () => new XlsxAdapter(),\n Renderer: XlsxRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|