@elabs-ai/components-viewer 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/{chunk-NMA57QZ7.js → chunk-XBOR2YXS.js} +2 -2
- package/dist/{chunk-NMA57QZ7.js.map → chunk-XBOR2YXS.js.map} +1 -1
- package/dist/{csv-adapter-6VU3FFVU.js → csv-adapter-7HOFZ5Z6.js} +2 -2
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/dist/{pptx-adapter-6GEQLS2Z.js → pptx-adapter-BONWFHYT.js} +2 -2
- package/dist/pptx-adapter-BONWFHYT.js.map +1 -0
- package/dist/{xlsx-adapter-CM2Y6AKQ.js → xlsx-adapter-O3XIVLFC.js} +2 -2
- package/package.json +5 -5
- package/src/__contract__/file-viewer.contract.test.tsx +49 -0
- package/src/adapters/pptx/pptx-adapter.tsx +1 -1
- package/src/components/sheet-table.tsx +1 -1
- package/src/file-viewer/file-viewer.stories.tsx +14 -0
- package/src/file-viewer/file-viewer.test.tsx +35 -1
- package/src/file-viewer/file-viewer.tsx +17 -7
- package/dist/pptx-adapter-6GEQLS2Z.js.map +0 -1
- /package/dist/{csv-adapter-6VU3FFVU.js.map → csv-adapter-7HOFZ5Z6.js.map} +0 -0
- /package/dist/{xlsx-adapter-CM2Y6AKQ.js.map → xlsx-adapter-O3XIVLFC.js.map} +0 -0
|
@@ -312,7 +312,7 @@ function PptxSlideView({
|
|
|
312
312
|
"aria-label": t("viewer.pptx.slide", {
|
|
313
313
|
slide: formatNumber(slideNumber2)
|
|
314
314
|
}),
|
|
315
|
-
className: "border-border bg-card focus-
|
|
315
|
+
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",
|
|
316
316
|
children: [
|
|
317
317
|
/* @__PURE__ */ jsx(ProseHeading, { level: baseHeadingLevel, children: slide.title === void 0 ? t("viewer.pptx.untitled") : /* @__PURE__ */ jsx(MarkedText, { text: slide.title, marks, start: starts?.get(PPTX_TITLE_LINE) }) }),
|
|
318
318
|
isEmpty ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-body", children: t("viewer.pptx.empty") }) : /* @__PURE__ */ jsx("ul", { className: "text-body space-y-1.5", children: slide.lines.map((line, lineIndex) => /* @__PURE__ */ jsx(
|
|
@@ -348,4 +348,4 @@ var pptx_adapter_default = adapterModule;
|
|
|
348
348
|
export {
|
|
349
349
|
pptx_adapter_default as default
|
|
350
350
|
};
|
|
351
|
-
//# sourceMappingURL=pptx-adapter-
|
|
351
|
+
//# sourceMappingURL=pptx-adapter-BONWFHYT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
SheetTable,
|
|
8
8
|
gridToText
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XBOR2YXS.js";
|
|
10
10
|
import "./chunk-2NQ4RSJ3.js";
|
|
11
11
|
import {
|
|
12
12
|
spanAt
|
|
@@ -164,4 +164,4 @@ export {
|
|
|
164
164
|
xlsx_adapter_default as default,
|
|
165
165
|
looksLikeWorkbook
|
|
166
166
|
};
|
|
167
|
-
//# sourceMappingURL=xlsx-adapter-
|
|
167
|
+
//# sourceMappingURL=xlsx-adapter-O3XIVLFC.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elabs-ai/components-viewer",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"jszip": "^3.10.1",
|
|
40
40
|
"shiki": "^3.22.0",
|
|
41
41
|
"streamdown": "^2.4.0",
|
|
42
|
-
"@elabs-ai/components-tokens": "4.
|
|
43
|
-
"@elabs-ai/components-ui": "4.
|
|
42
|
+
"@elabs-ai/components-tokens": "4.2.0",
|
|
43
|
+
"@elabs-ai/components-ui": "4.2.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"papaparse": {
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
"typescript": "^5.7.3",
|
|
89
89
|
"vitest": "^3.0.2",
|
|
90
90
|
"@elabs-ai/components-eslint-config": "0.1.0",
|
|
91
|
+
"@elabs-ai/components-tokens": "4.2.0",
|
|
91
92
|
"@elabs-ai/components-typescript-config": "0.1.0",
|
|
92
|
-
"@elabs-ai/components-
|
|
93
|
-
"@elabs-ai/components-ui": "4.0.0"
|
|
93
|
+
"@elabs-ai/components-ui": "4.2.0"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"build": "tsup && node ../../scripts/link-dist-css.mjs",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// GENERATED by scripts/gen-contract-tests.mjs — do not edit; re-run the generator instead.
|
|
2
|
+
/**
|
|
3
|
+
* Contract probe for FileViewer (`packages/viewer/src/file-viewer/file-viewer.tsx`), derived from its
|
|
4
|
+
* `Default` story (packages/viewer/src/file-viewer/file-viewer.stories.tsx). See scripts/gen-contract-tests.mjs.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
7
|
+
import { render, cleanup } from "@testing-library/react";
|
|
8
|
+
import { createRef } from "react";
|
|
9
|
+
import * as stories from "../file-viewer/file-viewer.stories";
|
|
10
|
+
import knownFailuresJson from "../../../../scripts/check/contract-known-failures.json";
|
|
11
|
+
|
|
12
|
+
afterEach(cleanup);
|
|
13
|
+
|
|
14
|
+
// Every component here has its own prop/ref/element shape; a generated probe
|
|
15
|
+
// stays generic on purpose (loosely typed, not untyped — see
|
|
16
|
+
// scripts/gen-contract-tests.mjs) rather than re-deriving each one.
|
|
17
|
+
const KNOWN_FAILURES: Record<string, string> = knownFailuresJson;
|
|
18
|
+
const meta = stories.default as { component?: unknown; args?: Record<string, unknown> };
|
|
19
|
+
const Default = (stories as { Default?: { args?: Record<string, unknown> } }).Default;
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- probe target; see comment above
|
|
21
|
+
const Component = meta.component as any;
|
|
22
|
+
const args = { ...(meta.args ?? {}), ...(Default?.args ?? {}) };
|
|
23
|
+
const isForwardRefComponent = Component?.["$$typeof"] === Symbol.for("react.forward_ref");
|
|
24
|
+
|
|
25
|
+
/** Wrap a known, tracked failure in `it.fails` so fixing it forces the key's removal. */
|
|
26
|
+
function contractIt(assertion: string, name: string, fn: () => void) {
|
|
27
|
+
const key = `viewer-fileviewer--default|jsdom|jsdom|${assertion}`;
|
|
28
|
+
const reason = KNOWN_FAILURES[key];
|
|
29
|
+
if (reason) return it.fails(`${name} (known failure: ${reason})`, fn);
|
|
30
|
+
return it(name, fn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("FileViewer contract", () => {
|
|
34
|
+
it.skipIf(!isForwardRefComponent)("forwards a ref to a DOM element", () => {
|
|
35
|
+
const ref = createRef<Element>();
|
|
36
|
+
render(<Component {...args} ref={ref} />);
|
|
37
|
+
expect(ref.current).toBeInstanceOf(Element);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
contractIt("className", "merges a caller className onto the root", () => {
|
|
41
|
+
const { container } = render(<Component {...args} className="contract-probe" />);
|
|
42
|
+
expect(container.querySelector(".contract-probe")).not.toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
contractIt("data-slot", 'exposes data-slot="file-viewer" on its root', () => {
|
|
46
|
+
const { container } = render(<Component {...args} />);
|
|
47
|
+
expect(container.querySelector('[data-slot="file-viewer"]')).not.toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -289,7 +289,7 @@ function PptxSlideView({
|
|
|
289
289
|
aria-label={t("viewer.pptx.slide", {
|
|
290
290
|
slide: formatNumber(slideNumber),
|
|
291
291
|
})}
|
|
292
|
-
className="border-border bg-card focus-
|
|
292
|
+
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"
|
|
293
293
|
>
|
|
294
294
|
{/* A slide title is the deck's top rung, so it sits at the host's base.
|
|
295
295
|
The stand-in for an untitled slide is OUR text, not the deck's, so
|
|
@@ -88,7 +88,7 @@ export function SheetTable({
|
|
|
88
88
|
tabIndex={0}
|
|
89
89
|
role="group"
|
|
90
90
|
aria-label={t("viewer.content")}
|
|
91
|
-
className="focus-
|
|
91
|
+
className="focus-ring min-h-0 flex-1 overflow-auto"
|
|
92
92
|
>
|
|
93
93
|
<Table>
|
|
94
94
|
<TableCaption className="sr-only">
|
|
@@ -199,6 +199,20 @@ export const PlainText: Story = {
|
|
|
199
199
|
* is a deliberate trade: fenced code renders as an unhighlighted block rather
|
|
200
200
|
* than costing a consumer four more packages to open one file. A source file
|
|
201
201
|
* opened directly still gets Shiki — see `Source code` below.
|
|
202
|
+
*
|
|
203
|
+
* Pick a markdown renderer by where the markdown is going to be READ: a file the
|
|
204
|
+
* app did not write, opened here → this adapter; a read-only document in a chat
|
|
205
|
+
* or a side rail → `AI/MarkdownView`; the preview pane of the markdown editor →
|
|
206
|
+
* `Editor/MarkdownPreview`; streaming into a message as the model writes it →
|
|
207
|
+
* `MessageResponse` on `AI/Message`. See
|
|
208
|
+
* [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).
|
|
209
|
+
*
|
|
210
|
+
* The element map here is deliberately a near-copy of `MarkdownView`'s, not a
|
|
211
|
+
* shared module: `@elabs-ai/components-ai`, `@elabs-ai/components-editor` and
|
|
212
|
+
* `@elabs-ai/components-viewer` are leaves that may not import one another, and
|
|
213
|
+
* the half that could move down (the `Prose*` primitives, the Streamdown locale
|
|
214
|
+
* bridge) already has. What differs is the job — a file arrives settled or not at
|
|
215
|
+
* all, so this adapter takes no plugins and does not stream.
|
|
202
216
|
*/
|
|
203
217
|
export const Markdown: Story = {
|
|
204
218
|
args: { source: { kind: "text", text: MARKDOWN, name: "README.md" } },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEvent, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
1
|
+
import { createEvent, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { useEffect } from "react";
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
@@ -210,6 +210,40 @@ describe("FileViewer — chrome", () => {
|
|
|
210
210
|
expect(screen.queryByRole("button", { name: /Download/ })).not.toBeInTheDocument();
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
it("surfaces a terminal error (role=alert) when the download read rejects, instead of swallowing it", async () => {
|
|
214
|
+
// A `url` source routes through `fetch` for `.bytes()` — reject it to
|
|
215
|
+
// exercise the toolbar's download failure path. (The main content pane
|
|
216
|
+
// fails to load too, from the same mocked fetch — scope assertions to the
|
|
217
|
+
// toolbar so the two independent error surfaces aren't conflated.)
|
|
218
|
+
const fetchMock = vi.fn().mockRejectedValue(new Error("network down"));
|
|
219
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
220
|
+
try {
|
|
221
|
+
render(
|
|
222
|
+
<FileViewer
|
|
223
|
+
source={{
|
|
224
|
+
kind: "url",
|
|
225
|
+
url: "https://example.com/notes.txt",
|
|
226
|
+
name: "notes.txt",
|
|
227
|
+
mediaType: "text/plain",
|
|
228
|
+
}}
|
|
229
|
+
registry={textOnlyRegistry()}
|
|
230
|
+
/>,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const toolbar = document.querySelector('[data-slot="file-viewer-toolbar"]') as HTMLElement;
|
|
234
|
+
const downloadButton = within(toolbar).getByRole("button", { name: "Download notes.txt" });
|
|
235
|
+
expect(within(toolbar).queryByRole("alert")).not.toBeInTheDocument();
|
|
236
|
+
|
|
237
|
+
await userEvent.click(downloadButton);
|
|
238
|
+
|
|
239
|
+
expect(await within(toolbar).findByRole("alert")).toHaveTextContent(
|
|
240
|
+
"notes.txt couldn't be read. It may have moved, or you may not have access.",
|
|
241
|
+
);
|
|
242
|
+
} finally {
|
|
243
|
+
vi.unstubAllGlobals();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
213
247
|
it("makes the scrolling content pane a real keyboard stop", async () => {
|
|
214
248
|
render(<FileViewer source={textSource} registry={textOnlyRegistry()} />);
|
|
215
249
|
await screen.findByText("hello world");
|
|
@@ -721,13 +721,21 @@ export const FileViewerToolbar = forwardRef<HTMLDivElement, FileViewerToolbarPro
|
|
|
721
721
|
const { t } = useLocale();
|
|
722
722
|
const source = state.source;
|
|
723
723
|
const Glyph = fileIconFor(source?.name ?? "", source?.mediaType);
|
|
724
|
+
const [downloadFailed, setDownloadFailed] = useState(false);
|
|
724
725
|
|
|
725
726
|
if (!source) return null;
|
|
726
727
|
|
|
727
728
|
const download = () => {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
setDownloadFailed(false);
|
|
730
|
+
void source
|
|
731
|
+
.bytes()
|
|
732
|
+
.then((bytes) => {
|
|
733
|
+
downloadBlob(new Blob([bytes], { type: source.mediaType }), source.name);
|
|
734
|
+
})
|
|
735
|
+
.catch((error: unknown) => {
|
|
736
|
+
console.error("FileViewer: download failed", error);
|
|
737
|
+
setDownloadFailed(true);
|
|
738
|
+
});
|
|
731
739
|
};
|
|
732
740
|
|
|
733
741
|
return (
|
|
@@ -749,6 +757,11 @@ export const FileViewerToolbar = forwardRef<HTMLDivElement, FileViewerToolbarPro
|
|
|
749
757
|
</Text>
|
|
750
758
|
{children}
|
|
751
759
|
{actions}
|
|
760
|
+
{downloadFailed ? (
|
|
761
|
+
<Text role="alert" className="text-destructive-text shrink-0 truncate">
|
|
762
|
+
{t("viewer.error.readFailedBody", { name: source.name, format: source.mediaType })}
|
|
763
|
+
</Text>
|
|
764
|
+
) : null}
|
|
752
765
|
<Separator orientation="vertical" className="h-4" />
|
|
753
766
|
{/* `label` is IconButton's single source of truth — it becomes both the
|
|
754
767
|
accessible name and the tooltip, so the two cannot drift. */}
|
|
@@ -1013,10 +1026,7 @@ export const FileViewerContent = forwardRef<HTMLDivElement, FileViewerContentPro
|
|
|
1013
1026
|
role="region"
|
|
1014
1027
|
aria-label={t("viewer.content")}
|
|
1015
1028
|
tabIndex={0}
|
|
1016
|
-
className={cn(
|
|
1017
|
-
"focus-visible:ring-ring min-h-0 flex-1 overflow-auto p-4 focus-visible:outline-none focus-visible:ring-2",
|
|
1018
|
-
className,
|
|
1019
|
-
)}
|
|
1029
|
+
className={cn("focus-ring min-h-0 flex-1 overflow-auto p-4", className)}
|
|
1020
1030
|
{...props}
|
|
1021
1031
|
>
|
|
1022
1032
|
{state.status === "empty" && <FileViewerEmpty />}
|
|
@@ -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-visible:ring-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm focus-visible:outline-none focus-visible:ring-2\"\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"]}
|
|
File without changes
|
|
File without changes
|