@elabs-ai/components-viewer 4.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/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/chunk-2NQ4RSJ3.js +18 -0
- package/dist/chunk-2NQ4RSJ3.js.map +1 -0
- package/dist/chunk-3U3MESBU.js +21 -0
- package/dist/chunk-3U3MESBU.js.map +1 -0
- package/dist/chunk-4OFQYALG.js +147 -0
- package/dist/chunk-4OFQYALG.js.map +1 -0
- package/dist/chunk-5OMONA2G.js +87 -0
- package/dist/chunk-5OMONA2G.js.map +1 -0
- package/dist/chunk-AORNMH77.js +58 -0
- package/dist/chunk-AORNMH77.js.map +1 -0
- package/dist/chunk-BCF3RWRL.js +18 -0
- package/dist/chunk-BCF3RWRL.js.map +1 -0
- package/dist/chunk-FELIAGKI.js +22 -0
- package/dist/chunk-FELIAGKI.js.map +1 -0
- package/dist/chunk-GGR7BNW4.js +18 -0
- package/dist/chunk-GGR7BNW4.js.map +1 -0
- package/dist/chunk-H3OAON3D.js +22 -0
- package/dist/chunk-H3OAON3D.js.map +1 -0
- package/dist/chunk-HMG2ERXH.js +17 -0
- package/dist/chunk-HMG2ERXH.js.map +1 -0
- package/dist/chunk-KOMDRWOU.js +21 -0
- package/dist/chunk-KOMDRWOU.js.map +1 -0
- package/dist/chunk-NMA57QZ7.js +117 -0
- package/dist/chunk-NMA57QZ7.js.map +1 -0
- package/dist/chunk-RGWDIQJR.js +26 -0
- package/dist/chunk-RGWDIQJR.js.map +1 -0
- package/dist/chunk-SLXRUYT3.js +91 -0
- package/dist/chunk-SLXRUYT3.js.map +1 -0
- package/dist/chunk-UJIJECEF.js +21 -0
- package/dist/chunk-UJIJECEF.js.map +1 -0
- package/dist/chunk-UL43NGUG.js +68 -0
- package/dist/chunk-UL43NGUG.js.map +1 -0
- package/dist/chunk-VO2273Z2.js +55 -0
- package/dist/chunk-VO2273Z2.js.map +1 -0
- package/dist/code-adapter-ADZ4UOGN.js +226 -0
- package/dist/code-adapter-ADZ4UOGN.js.map +1 -0
- package/dist/csv-adapter-6VU3FFVU.js +100 -0
- package/dist/csv-adapter-6VU3FFVU.js.map +1 -0
- package/dist/docx-adapter-5CQDHWTD.js +383 -0
- package/dist/docx-adapter-5CQDHWTD.js.map +1 -0
- package/dist/image-adapter-WOHZR24J.js +132 -0
- package/dist/image-adapter-WOHZR24J.js.map +1 -0
- package/dist/index.d.ts +1353 -0
- package/dist/index.js +1364 -0
- package/dist/index.js.map +1 -0
- package/dist/json-adapter-ZUW5GQHE.js +90 -0
- package/dist/json-adapter-ZUW5GQHE.js.map +1 -0
- package/dist/markdown-adapter-YC6WTBS4.js +214 -0
- package/dist/markdown-adapter-YC6WTBS4.js.map +1 -0
- package/dist/media-adapter-MCTB4GBH.js +78 -0
- package/dist/media-adapter-MCTB4GBH.js.map +1 -0
- package/dist/pdf-adapter-5PMKEXUD.js +424 -0
- package/dist/pdf-adapter-5PMKEXUD.js.map +1 -0
- package/dist/pptx-adapter-6GEQLS2Z.js +351 -0
- package/dist/pptx-adapter-6GEQLS2Z.js.map +1 -0
- package/dist/text-adapter-NFNWB5W3.js +81 -0
- package/dist/text-adapter-NFNWB5W3.js.map +1 -0
- package/dist/xlsx-adapter-CM2Y6AKQ.js +167 -0
- package/dist/xlsx-adapter-CM2Y6AKQ.js.map +1 -0
- package/package.json +103 -0
- package/src/adapters/code/code-adapter.test.tsx +221 -0
- package/src/adapters/code/code-adapter.tsx +272 -0
- package/src/adapters/code/code-language.test.ts +45 -0
- package/src/adapters/code/code-language.ts +100 -0
- package/src/adapters/code/code-manifest.ts +19 -0
- package/src/adapters/code/code-theme.test.ts +46 -0
- package/src/adapters/code/code-theme.ts +99 -0
- package/src/adapters/csv/csv-adapter.test.tsx +169 -0
- package/src/adapters/csv/csv-adapter.tsx +146 -0
- package/src/adapters/csv/csv-manifest.ts +20 -0
- package/src/adapters/docx/docx-adapter.test.tsx +192 -0
- package/src/adapters/docx/docx-adapter.tsx +328 -0
- package/src/adapters/docx/docx-manifest.ts +18 -0
- package/src/adapters/docx/docx-model.test.ts +181 -0
- package/src/adapters/docx/docx-model.ts +294 -0
- package/src/adapters/image/image-adapter.test.tsx +141 -0
- package/src/adapters/image/image-adapter.tsx +191 -0
- package/src/adapters/image/image-manifest.ts +15 -0
- package/src/adapters/index.ts +64 -0
- package/src/adapters/json/json-adapter.test.tsx +77 -0
- package/src/adapters/json/json-adapter.tsx +141 -0
- package/src/adapters/json/json-manifest.ts +11 -0
- package/src/adapters/markdown/markdown-adapter.test.tsx +228 -0
- package/src/adapters/markdown/markdown-adapter.tsx +353 -0
- package/src/adapters/markdown/markdown-manifest.ts +21 -0
- package/src/adapters/markdown/markdown-marks.test.ts +85 -0
- package/src/adapters/markdown/markdown-marks.ts +119 -0
- package/src/adapters/media/media-adapter.test.tsx +79 -0
- package/src/adapters/media/media-adapter.tsx +123 -0
- package/src/adapters/media/media-manifest.ts +17 -0
- package/src/adapters/office-fixture.ts +239 -0
- package/src/adapters/pdf/pdf-adapter.test.tsx +489 -0
- package/src/adapters/pdf/pdf-adapter.tsx +599 -0
- package/src/adapters/pdf/pdf-engine.ts +171 -0
- package/src/adapters/pdf/pdf-fixture.ts +27 -0
- package/src/adapters/pdf/pdf-manifest.ts +30 -0
- package/src/adapters/pdf/pdf-text.test.ts +171 -0
- package/src/adapters/pdf/pdf-text.ts +202 -0
- package/src/adapters/pptx/pptx-adapter.test.tsx +241 -0
- package/src/adapters/pptx/pptx-adapter.tsx +341 -0
- package/src/adapters/pptx/pptx-manifest.ts +19 -0
- package/src/adapters/pptx/pptx-model.test.ts +219 -0
- package/src/adapters/pptx/pptx-model.ts +248 -0
- package/src/adapters/text/text-adapter.test.tsx +125 -0
- package/src/adapters/text/text-adapter.tsx +117 -0
- package/src/adapters/text/text-manifest.ts +19 -0
- package/src/adapters/xlsx/xlsx-adapter.test.tsx +159 -0
- package/src/adapters/xlsx/xlsx-adapter.tsx +250 -0
- package/src/adapters/xlsx/xlsx-manifest.ts +26 -0
- package/src/components/grid-text.test.ts +68 -0
- package/src/components/grid-text.ts +77 -0
- package/src/components/marked-text.tsx +36 -0
- package/src/components/sheet-table.tsx +142 -0
- package/src/core/errors.ts +120 -0
- package/src/core/highlight-marks.test.ts +95 -0
- package/src/core/highlight-marks.ts +95 -0
- package/src/core/highlight-resolve.test.ts +208 -0
- package/src/core/highlight-resolve.ts +191 -0
- package/src/core/highlight.ts +125 -0
- package/src/core/registry.test.ts +172 -0
- package/src/core/registry.ts +188 -0
- package/src/core/scroll-host.ts +42 -0
- package/src/core/text-index.test.ts +95 -0
- package/src/core/text-index.ts +159 -0
- package/src/core/types.ts +285 -0
- package/src/core/use-highlight-scroll.ts +59 -0
- package/src/core/use-page-control.ts +47 -0
- package/src/core/use-paged-scroll.ts +194 -0
- package/src/core/use-viewport-size.ts +54 -0
- package/src/core/zoom.ts +56 -0
- package/src/file-viewer/file-viewer-context.tsx +239 -0
- package/src/file-viewer/file-viewer-find.tsx +204 -0
- package/src/file-viewer/file-viewer-pager.tsx +120 -0
- package/src/file-viewer/file-viewer-zoom.tsx +166 -0
- package/src/file-viewer/file-viewer.stories.tsx +947 -0
- package/src/file-viewer/file-viewer.test.tsx +947 -0
- package/src/file-viewer/file-viewer.tsx +1143 -0
- package/src/index.ts +157 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/docx/docx-manifest.ts
|
|
7
|
+
var docxManifest = {
|
|
8
|
+
id: "docx",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
extensions: ["docx"],
|
|
11
|
+
mediaTypes: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
12
|
+
// No `rect`: a Word document has no page geometry here at all — mammoth
|
|
13
|
+
// discards Word's layout, and the renderer reflows the blocks into this
|
|
14
|
+
// system's prose, so a box on "page 3" would point at nothing.
|
|
15
|
+
capabilities: { text: true, highlight: ["quote", "range"] },
|
|
16
|
+
requires: ["mammoth"]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
docxManifest
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=chunk-FELIAGKI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/docx/docx-manifest.ts"],"sourcesContent":["import { type AdapterManifest, PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Word documents. `.doc` (the pre-2007 binary format) is deliberately absent:\n * mammoth reads OOXML only, and claiming an extension we cannot open would trade\n * an honest \"can't preview this file type\" panel for a parse error.\n */\nexport const docxManifest: AdapterManifest = {\n id: \"docx\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"docx\"],\n mediaTypes: [\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"],\n // No `rect`: a Word document has no page geometry here at all — mammoth\n // discards Word's layout, and the renderer reflows the blocks into this\n // system's prose, so a box on \"page 3\" would point at nothing.\n capabilities: { text: true, highlight: [\"quote\", \"range\"] },\n requires: [\"mammoth\"],\n};\n"],"mappings":";;;;;;AAOO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,MAAM;AAAA,EACnB,YAAY,CAAC,yEAAyE;AAAA;AAAA;AAAA;AAAA,EAItF,cAAc,EAAE,MAAM,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EAC1D,UAAU,CAAC,SAAS;AACtB;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/image/image-manifest.ts
|
|
7
|
+
var imageManifest = {
|
|
8
|
+
id: "image",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
categories: ["image"],
|
|
11
|
+
mediaTypes: ["image/"],
|
|
12
|
+
capabilities: { zoom: true, rotate: true }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
imageManifest
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=chunk-GGR7BNW4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/image/image-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only. Split from the adapter module so `createDefaultRegistry`\n * can answer \"can this be opened, and what controls apply\" without pulling the\n * renderer into the entry chunk.\n */\nexport const imageManifest: AdapterManifest = {\n id: \"image\",\n protocol: PROTOCOL_VERSION,\n categories: [\"image\"],\n mediaTypes: [\"image/\"],\n capabilities: { zoom: true, rotate: true },\n};\n"],"mappings":";;;;;;AAQO,IAAM,gBAAiC;AAAA,EAC5C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,OAAO;AAAA,EACpB,YAAY,CAAC,QAAQ;AAAA,EACrB,cAAc,EAAE,MAAM,MAAM,QAAQ,KAAK;AAC3C;","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/markdown/markdown-manifest.ts
|
|
7
|
+
var markdownManifest = {
|
|
8
|
+
id: "markdown",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
extensions: ["md", "markdown", "mdown", "mkd"],
|
|
11
|
+
mediaTypes: ["text/markdown"],
|
|
12
|
+
// Both kinds address the SOURCE, which is what `text` is here — and both are
|
|
13
|
+
// painted as a whole-block plate rather than as characters, because a source
|
|
14
|
+
// offset can land inside markup the reader never sees. See `markdown-marks.ts`.
|
|
15
|
+
capabilities: { text: true, search: true, highlight: ["quote", "range"] },
|
|
16
|
+
requires: ["streamdown"]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
markdownManifest
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=chunk-H3OAON3D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/markdown/markdown-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only (see `image-manifest.ts`).\n *\n * `.mdx` is deliberately NOT claimed: it is JavaScript wearing markdown, and\n * rendering it would mean evaluating a foreign file's code — the one thing a\n * viewer must never do. An `.mdx` falls through to the plain-text adapter.\n */\nexport const markdownManifest: AdapterManifest = {\n id: \"markdown\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"md\", \"markdown\", \"mdown\", \"mkd\"],\n mediaTypes: [\"text/markdown\"],\n // Both kinds address the SOURCE, which is what `text` is here — and both are\n // painted as a whole-block plate rather than as characters, because a source\n // offset can land inside markup the reader never sees. See `markdown-marks.ts`.\n capabilities: { text: true, search: true, highlight: [\"quote\", \"range\"] },\n requires: [\"streamdown\"],\n};\n"],"mappings":";;;;;;AAUO,IAAM,mBAAoC;AAAA,EAC/C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,MAAM,YAAY,SAAS,KAAK;AAAA,EAC7C,YAAY,CAAC,eAAe;AAAA;AAAA;AAAA;AAAA,EAI5B,cAAc,EAAE,MAAM,MAAM,QAAQ,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EACxE,UAAU,CAAC,YAAY;AACzB;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/media/media-manifest.ts
|
|
7
|
+
var mediaManifest = {
|
|
8
|
+
id: "media",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
categories: ["video", "audio"],
|
|
11
|
+
mediaTypes: ["video/", "audio/"]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
mediaManifest
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=chunk-HMG2ERXH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/media/media-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only.\n *\n * No `requires`: the browser is the decoder, so this adapter has no optional\n * peer and can never report `parser-missing`. A codec the browser cannot play is\n * a different failure — the `<video>` element's own `error` event — and it is\n * reported where it happens, in the renderer.\n */\nexport const mediaManifest: AdapterManifest = {\n id: \"media\",\n protocol: PROTOCOL_VERSION,\n categories: [\"video\", \"audio\"],\n mediaTypes: [\"video/\", \"audio/\"],\n};\n"],"mappings":";;;;;;AAWO,IAAM,gBAAiC;AAAA,EAC5C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,SAAS,OAAO;AAAA,EAC7B,YAAY,CAAC,UAAU,QAAQ;AACjC;","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/pptx/pptx-manifest.ts
|
|
7
|
+
var pptxManifest = {
|
|
8
|
+
id: "pptx",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
extensions: ["pptx"],
|
|
11
|
+
mediaTypes: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
|
|
12
|
+
// No `rect`: the deck is read as an OUTLINE, not reproduced as a canvas, so
|
|
13
|
+
// there is no slide geometry a box could point into (`pptx-model.ts`).
|
|
14
|
+
capabilities: { pages: true, text: true, highlight: ["quote", "range"] },
|
|
15
|
+
requires: ["jszip"]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
pptxManifest
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=chunk-KOMDRWOU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/pptx/pptx-manifest.ts"],"sourcesContent":["import { type AdapterManifest, PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * PowerPoint decks. `.ppt` (the pre-2007 binary format) is not a zip at all, so\n * it is deliberately absent rather than claimed and then failed on.\n *\n * `requires: [\"jszip\"]` — the deck is unzipped here and the XML is parsed with\n * the platform's own `DOMParser`, so there is no PowerPoint library to install.\n */\nexport const pptxManifest: AdapterManifest = {\n id: \"pptx\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"pptx\"],\n mediaTypes: [\"application/vnd.openxmlformats-officedocument.presentationml.presentation\"],\n // No `rect`: the deck is read as an OUTLINE, not reproduced as a canvas, so\n // there is no slide geometry a box could point into (`pptx-model.ts`).\n capabilities: { pages: true, text: true, highlight: [\"quote\", \"range\"] },\n requires: [\"jszip\"],\n};\n"],"mappings":";;;;;;AASO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,MAAM;AAAA,EACnB,YAAY,CAAC,2EAA2E;AAAA;AAAA;AAAA,EAGxF,cAAc,EAAE,OAAO,MAAM,MAAM,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EACvE,UAAU,CAAC,OAAO;AACpB;","names":[]}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
MarkedText
|
|
4
|
+
} from "./chunk-2NQ4RSJ3.js";
|
|
5
|
+
import {
|
|
6
|
+
chunkOffset,
|
|
7
|
+
createTextIndexBuilder
|
|
8
|
+
} from "./chunk-UL43NGUG.js";
|
|
9
|
+
import {
|
|
10
|
+
useScrollActiveHighlightIntoView
|
|
11
|
+
} from "./chunk-VO2273Z2.js";
|
|
12
|
+
|
|
13
|
+
// src/components/grid-text.ts
|
|
14
|
+
var GRID_CELL_SEPARATOR = " ";
|
|
15
|
+
var GRID_ROW_SEPARATOR = "\n";
|
|
16
|
+
var GRID_SHEET_SEPARATOR = "\n\n";
|
|
17
|
+
var GRID_NAME_ROW = -2;
|
|
18
|
+
var GRID_HEAD_ROW = -1;
|
|
19
|
+
function gridToText(sheets) {
|
|
20
|
+
const builder = createTextIndexBuilder({ separator: GRID_ROW_SEPARATOR });
|
|
21
|
+
sheets.forEach((sheet, index) => {
|
|
22
|
+
let separator = index === 0 ? void 0 : GRID_SHEET_SEPARATOR;
|
|
23
|
+
const push = (chunk, row) => {
|
|
24
|
+
builder.push(chunk, { sheet: index, row }, separator);
|
|
25
|
+
if (chunk.length > 0) separator = void 0;
|
|
26
|
+
};
|
|
27
|
+
if (sheet.name) push(sheet.name, GRID_NAME_ROW);
|
|
28
|
+
push(sheet.columns.join(GRID_CELL_SEPARATOR), GRID_HEAD_ROW);
|
|
29
|
+
sheet.rows.forEach((row, rowIndex) => {
|
|
30
|
+
push(row.join(GRID_CELL_SEPARATOR), rowIndex);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return builder.build();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/components/sheet-table.tsx
|
|
37
|
+
import {
|
|
38
|
+
cn,
|
|
39
|
+
Table,
|
|
40
|
+
TableBody,
|
|
41
|
+
TableCaption,
|
|
42
|
+
TableCell,
|
|
43
|
+
TableHead,
|
|
44
|
+
TableHeader,
|
|
45
|
+
TableRow,
|
|
46
|
+
useLocale
|
|
47
|
+
} from "@elabs-ai/components-ui";
|
|
48
|
+
import { useRef } from "react";
|
|
49
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
50
|
+
function SheetTable({
|
|
51
|
+
columns,
|
|
52
|
+
rows,
|
|
53
|
+
totalRows,
|
|
54
|
+
className,
|
|
55
|
+
marks,
|
|
56
|
+
rowStart,
|
|
57
|
+
activeHighlightId
|
|
58
|
+
}) {
|
|
59
|
+
const { t, formatNumber } = useLocale();
|
|
60
|
+
const viewport = useRef(null);
|
|
61
|
+
useScrollActiveHighlightIntoView(viewport, activeHighlightId);
|
|
62
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 flex-col gap-2", className), children: [
|
|
63
|
+
totalRows !== void 0 && // A status, not an error: the file is fine, we are simply showing part
|
|
64
|
+
// of it (loading-states.md — a capability bound is news, not an alarm).
|
|
65
|
+
/* @__PURE__ */ jsx("p", { role: "status", className: "text-meta text-muted-foreground shrink-0", children: t("viewer.table.truncated", { count: formatNumber(rows.length) }) }),
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
69
|
+
ref: viewport,
|
|
70
|
+
tabIndex: 0,
|
|
71
|
+
role: "group",
|
|
72
|
+
"aria-label": t("viewer.content"),
|
|
73
|
+
className: "focus-visible:ring-ring min-h-0 flex-1 overflow-auto focus-visible:outline-none focus-visible:ring-2",
|
|
74
|
+
children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
75
|
+
/* @__PURE__ */ jsx(TableCaption, { className: "sr-only", children: t("viewer.table.caption", {
|
|
76
|
+
rows: formatNumber(totalRows ?? rows.length),
|
|
77
|
+
columns: formatNumber(columns.length)
|
|
78
|
+
}) }),
|
|
79
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column, index) => (
|
|
80
|
+
// The file's own header text is the only identity a column has;
|
|
81
|
+
// an empty one still needs a cell so the grid stays aligned.
|
|
82
|
+
/* @__PURE__ */ jsx(TableHead, { scope: "col", children: /* @__PURE__ */ jsx(
|
|
83
|
+
MarkedText,
|
|
84
|
+
{
|
|
85
|
+
text: column,
|
|
86
|
+
marks,
|
|
87
|
+
start: chunkOffset(
|
|
88
|
+
columns,
|
|
89
|
+
index,
|
|
90
|
+
rowStart?.(GRID_HEAD_ROW),
|
|
91
|
+
GRID_CELL_SEPARATOR
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
) }, `${column}-${String(index)}`)
|
|
95
|
+
)) }) }),
|
|
96
|
+
/* @__PURE__ */ jsx(TableBody, { children: rows.map((row, rowIndex) => {
|
|
97
|
+
const start = rowStart?.(rowIndex);
|
|
98
|
+
return /* @__PURE__ */ jsx(TableRow, { children: columns.map((_, columnIndex) => /* @__PURE__ */ jsx(TableCell, { className: "whitespace-pre-wrap", children: /* @__PURE__ */ jsx(
|
|
99
|
+
MarkedText,
|
|
100
|
+
{
|
|
101
|
+
text: row[columnIndex] ?? "",
|
|
102
|
+
marks,
|
|
103
|
+
start: chunkOffset(row, columnIndex, start, GRID_CELL_SEPARATOR)
|
|
104
|
+
}
|
|
105
|
+
) }, columnIndex)) }, rowIndex);
|
|
106
|
+
}) })
|
|
107
|
+
] })
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
] });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
gridToText,
|
|
115
|
+
SheetTable
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=chunk-NMA57QZ7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/grid-text.ts","../src/components/sheet-table.tsx"],"sourcesContent":["/**\n * The one text projection every tabular format is addressed against.\n *\n * CSV and XLSX arrive through different parsers and render into the same\n * `SheetTable`; they should also be *addressable* the same way, or a citation\n * that resolves in a workbook would miss in the CSV export of the same data.\n * So the projection is written once here and both adapters call it.\n *\n * Shape — a sheet's name (when it has one), then its header row, then its body\n * rows; cells joined by a tab, rows by a newline, sheets by a blank line:\n *\n * ```\n * Q3\n * Region\\tRevenue\n * EMEA\\t4.2M\n *\n * Q4\n * …\n * ```\n *\n * **Rows, not cells, are the finest ref** — the same trade the Word adapter\n * makes. A cell-granular index would need a span per cell and a separator per\n * level; a row-granular one needs neither, because a cell's offset is the sum of\n * the cells before it ({@link chunkOffset}). The granularity of the REF is the\n * row; the granularity of the MARK is still the character.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../core/text-index\";\n\n/** Between two cells of the same row. */\nexport const GRID_CELL_SEPARATOR = \"\\t\";\n/** Between two rows of the same sheet. */\nexport const GRID_ROW_SEPARATOR = \"\\n\";\n/** Between two sheets of the same workbook. */\nexport const GRID_SHEET_SEPARATOR = \"\\n\\n\";\n\n/** The `row` of a sheet's own name line. */\nexport const GRID_NAME_ROW = -2;\n/** The `row` of a sheet's header row. */\nexport const GRID_HEAD_ROW = -1;\n\n/** Where a stretch of the projection came from in the grid. */\nexport interface GridRef {\n /** Index into the sheets. `0` for a single-sheet format like CSV. */\n sheet: number;\n /** Body row index, or {@link GRID_HEAD_ROW} / {@link GRID_NAME_ROW}. */\n row: number;\n}\n\n/** What {@link gridToText} needs from a sheet — the shape both adapters already hold. */\nexport interface GridSheetInput {\n /** The tab's name. Absent for a single-sheet format, which has no tab. */\n name?: string;\n columns: readonly string[];\n rows: readonly (readonly string[])[];\n}\n\n/** Project a workbook (or a one-sheet CSV) to text, with the map back to it. */\nexport function gridToText(sheets: readonly GridSheetInput[]): TextIndex<GridRef> {\n const builder = createTextIndexBuilder<GridRef>({ separator: GRID_ROW_SEPARATOR });\n sheets.forEach((sheet, index) => {\n // The blank line goes before whatever this sheet's FIRST line turns out to\n // be — its name, or its header when it has no name.\n let separator = index === 0 ? undefined : GRID_SHEET_SEPARATOR;\n const push = (chunk: string, row: number) => {\n builder.push(chunk, { sheet: index, row }, separator);\n if (chunk.length > 0) separator = undefined;\n };\n\n if (sheet.name) push(sheet.name, GRID_NAME_ROW);\n push(sheet.columns.join(GRID_CELL_SEPARATOR), GRID_HEAD_ROW);\n sheet.rows.forEach((row, rowIndex) => {\n push(row.join(GRID_CELL_SEPARATOR), rowIndex);\n });\n });\n return builder.build();\n}\n","\"use client\";\n\n/**\n * The one grid every tabular format renders into.\n *\n * CSV and XLSX arrive through completely different parsers and end up wanting\n * exactly the same thing: a header row, body rows, a truncation notice and an\n * `sr-only` caption. That is a PATTERN, not a coincidence, so it is named once\n * here rather than copied a second time (`.claude/rules/design-first.md` —\n * patterns over instances).\n *\n * It stays deliberately dumb: no sorting, no filtering, no virtualization. A\n * preview pane is for looking; past a few thousand rows the right component is\n * `DataTable` in `@elabs-ai/components-data`, which virtualizes.\n */\n\nimport {\n cn,\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { useRef } from \"react\";\n\nimport { type MarkRanges } from \"../core/highlight-marks\";\nimport { chunkOffset } from \"../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../core/use-highlight-scroll\";\nimport { GRID_CELL_SEPARATOR, GRID_HEAD_ROW } from \"./grid-text\";\nimport { MarkedText } from \"./marked-text\";\n\nexport interface SheetTableProps {\n /** Header cells. May be empty — the grid still renders its body. */\n columns: string[];\n /** Body rows, already capped by the caller. */\n rows: string[][];\n /** Total body rows in the file, when more exist than `rows` holds. */\n totalRows?: number;\n className?: string;\n /** Marks to paint, in projection offsets. */\n marks?: MarkRanges;\n /**\n * Where a row's first cell begins in the projection — {@link GRID_HEAD_ROW}\n * for the header. A function rather than an array so a workbook can answer\n * for the sheet being drawn without slicing an index per tab.\n */\n rowStart?: (row: number) => number | undefined;\n /** Scrolls the current mark into this grid's own viewport when it changes. */\n activeHighlightId?: string | null;\n}\n\nexport function SheetTable({\n columns,\n rows,\n totalRows,\n className,\n marks,\n rowStart,\n activeHighlightId,\n}: SheetTableProps) {\n const { t, formatNumber } = useLocale();\n const viewport = useRef<HTMLDivElement>(null);\n\n useScrollActiveHighlightIntoView(viewport, activeHighlightId);\n\n return (\n <div className={cn(\"flex h-full min-h-0 flex-col gap-2\", className)}>\n {totalRows !== undefined && (\n // A status, not an error: the file is fine, we are simply showing part\n // of it (loading-states.md — a capability bound is news, not an alarm).\n <p role=\"status\" className=\"text-meta text-muted-foreground shrink-0\">\n {t(\"viewer.table.truncated\", { count: formatNumber(rows.length) })}\n </p>\n )}\n {/* The grid keeps its OWN viewport, unlike the flowing formats: a workbook\n puts a sheet-tab bar above it that must not scroll away with the rows.\n A table cell is not focusable, so a scrollable region wrapping one is\n unreachable from a keyboard (WCAG 2.1.1) — `tabIndex={0}` makes it a\n real stop that arrow keys and Page Up/Down drive. `group`, not\n `region`, so a sheet does not mint a second landmark inside the\n viewer's content region. */}\n <div\n ref={viewport}\n tabIndex={0}\n role=\"group\"\n aria-label={t(\"viewer.content\")}\n className=\"focus-visible:ring-ring min-h-0 flex-1 overflow-auto focus-visible:outline-none focus-visible:ring-2\"\n >\n <Table>\n <TableCaption className=\"sr-only\">\n {t(\"viewer.table.caption\", {\n rows: formatNumber(totalRows ?? rows.length),\n columns: formatNumber(columns.length),\n })}\n </TableCaption>\n <TableHeader>\n <TableRow>\n {columns.map((column, index) => (\n // The file's own header text is the only identity a column has;\n // an empty one still needs a cell so the grid stays aligned.\n <TableHead key={`${column}-${String(index)}`} scope=\"col\">\n <MarkedText\n text={column}\n marks={marks}\n start={chunkOffset(\n columns,\n index,\n rowStart?.(GRID_HEAD_ROW),\n GRID_CELL_SEPARATOR,\n )}\n />\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row, rowIndex) => {\n const start = rowStart?.(rowIndex);\n return (\n <TableRow key={rowIndex}>\n {columns.map((_, columnIndex) => (\n <TableCell key={columnIndex} className=\"whitespace-pre-wrap\">\n <MarkedText\n text={row[columnIndex] ?? \"\"}\n marks={marks}\n start={chunkOffset(row, columnIndex, start, GRID_CELL_SEPARATOR)}\n />\n </TableCell>\n ))}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA8BO,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB;AAE3B,IAAM,uBAAuB;AAG7B,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAmBtB,SAAS,WAAW,QAAuD;AAChF,QAAM,UAAU,uBAAgC,EAAE,WAAW,mBAAmB,CAAC;AACjF,SAAO,QAAQ,CAAC,OAAO,UAAU;AAG/B,QAAI,YAAY,UAAU,IAAI,SAAY;AAC1C,UAAM,OAAO,CAAC,OAAe,QAAgB;AAC3C,cAAQ,KAAK,OAAO,EAAE,OAAO,OAAO,IAAI,GAAG,SAAS;AACpD,UAAI,MAAM,SAAS,EAAG,aAAY;AAAA,IACpC;AAEA,QAAI,MAAM,KAAM,MAAK,MAAM,MAAM,aAAa;AAC9C,SAAK,MAAM,QAAQ,KAAK,mBAAmB,GAAG,aAAa;AAC3D,UAAM,KAAK,QAAQ,CAAC,KAAK,aAAa;AACpC,WAAK,IAAI,KAAK,mBAAmB,GAAG,QAAQ;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AACD,SAAO,QAAQ,MAAM;AACvB;;;AC5DA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AA+Cf,cAkBA,YAlBA;AAnBD,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAoB;AAClB,QAAM,EAAE,GAAG,aAAa,IAAI,UAAU;AACtC,QAAM,WAAW,OAAuB,IAAI;AAE5C,mCAAiC,UAAU,iBAAiB;AAE5D,SACE,qBAAC,SAAI,WAAW,GAAG,sCAAsC,SAAS,GAC/D;AAAA,kBAAc;AAAA;AAAA,IAGb,oBAAC,OAAE,MAAK,UAAS,WAAU,4CACxB,YAAE,0BAA0B,EAAE,OAAO,aAAa,KAAK,MAAM,EAAE,CAAC,GACnE;AAAA,IASF;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,UAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAY,EAAE,gBAAgB;AAAA,QAC9B,WAAU;AAAA,QAEV,+BAAC,SACC;AAAA,8BAAC,gBAAa,WAAU,WACrB,YAAE,wBAAwB;AAAA,YACzB,MAAM,aAAa,aAAa,KAAK,MAAM;AAAA,YAC3C,SAAS,aAAa,QAAQ,MAAM;AAAA,UACtC,CAAC,GACH;AAAA,UACA,oBAAC,eACC,8BAAC,YACE,kBAAQ,IAAI,CAAC,QAAQ;AAAA;AAAA;AAAA,YAGpB,oBAAC,aAA6C,OAAM,OAClD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN;AAAA,gBACA,OAAO;AAAA,kBACL;AAAA,kBACA;AAAA,kBACA,WAAW,aAAa;AAAA,kBACxB;AAAA,gBACF;AAAA;AAAA,YACF,KAVc,GAAG,MAAM,IAAI,OAAO,KAAK,CAAC,EAW1C;AAAA,WACD,GACH,GACF;AAAA,UACA,oBAAC,aACE,eAAK,IAAI,CAAC,KAAK,aAAa;AAC3B,kBAAM,QAAQ,WAAW,QAAQ;AACjC,mBACE,oBAAC,YACE,kBAAQ,IAAI,CAAC,GAAG,gBACf,oBAAC,aAA4B,WAAU,uBACrC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,IAAI,WAAW,KAAK;AAAA,gBAC1B;AAAA,gBACA,OAAO,YAAY,KAAK,aAAa,OAAO,mBAAmB;AAAA;AAAA,YACjE,KALc,WAMhB,CACD,KATY,QAUf;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/xlsx/xlsx-manifest.ts
|
|
7
|
+
var xlsxManifest = {
|
|
8
|
+
id: "xlsx",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
extensions: ["xlsx", "xlsm", "xls", "ods"],
|
|
11
|
+
mediaTypes: [
|
|
12
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
13
|
+
"application/vnd.ms-excel",
|
|
14
|
+
"application/vnd.oasis.opendocument.spreadsheet"
|
|
15
|
+
],
|
|
16
|
+
// Addressed against the same grid projection the CSV adapter uses
|
|
17
|
+
// (`gridToText`), so a citation resolves the same way in a workbook and in the
|
|
18
|
+
// CSV export of one sheet. No `rect`: a cell has no page geometry.
|
|
19
|
+
capabilities: { pages: true, text: true, highlight: ["quote", "range"] },
|
|
20
|
+
requires: ["xlsx"]
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
xlsxManifest
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=chunk-RGWDIQJR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/xlsx/xlsx-manifest.ts"],"sourcesContent":["import { type AdapterManifest, PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Excel workbooks. Data-only, eager — see `core/types.ts` for why the manifest\n * and the loader are split.\n *\n * `.csv` is deliberately NOT claimed here even though SheetJS can read it: the\n * `csv` adapter is a better answer for a delimited text file (it detects the\n * delimiter and needs a far smaller parser), and claiming it would make which\n * adapter wins depend on registration order.\n */\nexport const xlsxManifest: AdapterManifest = {\n id: \"xlsx\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"xlsx\", \"xlsm\", \"xls\", \"ods\"],\n mediaTypes: [\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-excel\",\n \"application/vnd.oasis.opendocument.spreadsheet\",\n ],\n // Addressed against the same grid projection the CSV adapter uses\n // (`gridToText`), so a citation resolves the same way in a workbook and in the\n // CSV export of one sheet. No `rect`: a cell has no page geometry.\n capabilities: { pages: true, text: true, highlight: [\"quote\", \"range\"] },\n requires: [\"xlsx\"],\n};\n"],"mappings":";;;;;;AAWO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,QAAQ,QAAQ,OAAO,KAAK;AAAA,EACzC,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAIA,cAAc,EAAE,OAAO,MAAM,MAAM,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EACvE,UAAU,CAAC,MAAM;AACnB;","names":[]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/pdf/pdf-manifest.ts
|
|
7
|
+
var pdfManifest = {
|
|
8
|
+
id: "pdf",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
extensions: ["pdf"],
|
|
11
|
+
mediaTypes: ["application/pdf"],
|
|
12
|
+
capabilities: {
|
|
13
|
+
pages: true,
|
|
14
|
+
zoom: true,
|
|
15
|
+
text: true,
|
|
16
|
+
highlight: ["quote", "range", "rect"]
|
|
17
|
+
},
|
|
18
|
+
requires: ["pdfjs-dist"]
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/core/zoom.ts
|
|
22
|
+
var VIEWER_ZOOM_STEPS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
|
|
23
|
+
var DEFAULT_ZOOM = 1;
|
|
24
|
+
var EPSILON = 1e-6;
|
|
25
|
+
function isZoomFit(zoom) {
|
|
26
|
+
return typeof zoom === "string";
|
|
27
|
+
}
|
|
28
|
+
function stepZoom(from, delta) {
|
|
29
|
+
const steps = VIEWER_ZOOM_STEPS;
|
|
30
|
+
const first = steps[0];
|
|
31
|
+
const last = steps[steps.length - 1];
|
|
32
|
+
if (delta === 1) return steps.find((step) => step > from + EPSILON) ?? last;
|
|
33
|
+
return [...steps].reverse().find((step) => step < from - EPSILON) ?? first;
|
|
34
|
+
}
|
|
35
|
+
function canStepZoom(from, delta) {
|
|
36
|
+
const steps = VIEWER_ZOOM_STEPS;
|
|
37
|
+
return delta === 1 ? from < steps[steps.length - 1] - EPSILON : from > steps[0] + EPSILON;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/adapters/pdf/pdf-engine.ts
|
|
41
|
+
var config = {};
|
|
42
|
+
function configurePdfEngine(next) {
|
|
43
|
+
config = { ...config, ...next };
|
|
44
|
+
}
|
|
45
|
+
function getPdfEngineConfig() {
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
var enginePromise;
|
|
49
|
+
async function loadPdfEngine() {
|
|
50
|
+
enginePromise ??= (async () => {
|
|
51
|
+
const pdfjs = await import("pdfjs-dist");
|
|
52
|
+
pdfjs.GlobalWorkerOptions.workerSrc = config.workerSrc ?? defaultWorkerSrc();
|
|
53
|
+
return pdfjs;
|
|
54
|
+
})().catch((error) => {
|
|
55
|
+
enginePromise = void 0;
|
|
56
|
+
throw error;
|
|
57
|
+
});
|
|
58
|
+
return enginePromise;
|
|
59
|
+
}
|
|
60
|
+
function defaultWorkerSrc() {
|
|
61
|
+
return new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).href;
|
|
62
|
+
}
|
|
63
|
+
async function openPdfDocument(bytes, signal) {
|
|
64
|
+
const pdfjs = await loadPdfEngine();
|
|
65
|
+
const task = pdfjs.getDocument({
|
|
66
|
+
data: bytes,
|
|
67
|
+
// Defence in depth: pdf.js can evaluate font programs, and a viewer opens
|
|
68
|
+
// files the app did not write. The cost is a rare, slightly slower font
|
|
69
|
+
// path; the benefit is that a malicious PDF has no `eval` to reach for.
|
|
70
|
+
isEvalSupported: false,
|
|
71
|
+
cMapUrl: config.cMapUrl,
|
|
72
|
+
cMapPacked: true,
|
|
73
|
+
standardFontDataUrl: config.standardFontDataUrl
|
|
74
|
+
});
|
|
75
|
+
signal?.addEventListener("abort", () => void task.destroy(), { once: true });
|
|
76
|
+
const document = await task.promise;
|
|
77
|
+
return { document, destroy: () => task.destroy() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
pdfManifest,
|
|
82
|
+
VIEWER_ZOOM_STEPS,
|
|
83
|
+
DEFAULT_ZOOM,
|
|
84
|
+
isZoomFit,
|
|
85
|
+
stepZoom,
|
|
86
|
+
canStepZoom,
|
|
87
|
+
configurePdfEngine,
|
|
88
|
+
getPdfEngineConfig,
|
|
89
|
+
openPdfDocument
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=chunk-SLXRUYT3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/pdf/pdf-manifest.ts","../src/core/zoom.ts","../src/adapters/pdf/pdf-engine.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only. Split from the adapter module so `createDefaultRegistry`\n * can answer \"can this be opened, and what controls apply\" without pulling\n * pdf.js — a ~1 MB engine — into the entry chunk.\n *\n * `requires` is what the \"install this\" message names when `pdfjs-dist` is not\n * present: the adapter is registered either way, so the reader is told what is\n * missing instead of being told the file type is unsupported.\n *\n * The only adapter that declares `rect`: a PDF page has a fixed coordinate\n * space, so a producer that knows where a passage sits on the page — an OCR\n * pipeline, a layout-aware chunker — can address it geometrically and skip the\n * text projection entirely.\n */\nexport const pdfManifest: AdapterManifest = {\n id: \"pdf\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"pdf\"],\n mediaTypes: [\"application/pdf\"],\n capabilities: {\n pages: true,\n zoom: true,\n text: true,\n highlight: [\"quote\", \"range\", \"rect\"],\n },\n requires: [\"pdfjs-dist\"],\n};\n","/**\n * The zoom vocabulary — the stops, and how the chrome moves between them.\n *\n * Lives in `core/` rather than in the PDF adapter (where the stops started)\n * because zoom is now provider state: the shell's zoom control, the provider's\n * `zoomIn`/`zoomOut` and every adapter that scales its content all have to agree\n * on the same ladder, or \"125%\" in the toolbar and the scale the page rendered\n * at drift apart.\n */\n\nimport type { ZoomFit, ZoomLevel } from \"./types\";\n\n/**\n * Zoom stops, in document scale.\n *\n * Discrete rather than continuous: a slider is imprecise with a pointer and\n * impossible with a keyboard at this size, and \"somewhere near 137%\" is not a\n * state a reader ever wants to be in. The ladder is the one from the original\n * PDF pager, unchanged, so existing screens land on the same numbers.\n */\nexport const VIEWER_ZOOM_STEPS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3] as const;\n\n/** 100% — the scale a document opens at. */\nexport const DEFAULT_ZOOM = 1;\n\n/** Float slack, so `1.0000000000000002 > 1` does not skip a stop. */\nconst EPSILON = 1e-6;\n\n/** Whether a zoom level is a fit mode rather than a fixed scale. */\nexport function isZoomFit(zoom: ZoomLevel): zoom is ZoomFit {\n return typeof zoom === \"string\";\n}\n\n/**\n * The next stop above or below `from`.\n *\n * Takes an arbitrary scale, not an index, because a fit mode resolves to\n * whatever the viewport made it (1.37, say) and the reader's next \"zoom in\" has\n * to land on a real stop above that — an index-based pager would jump back to\n * wherever the ladder was last parked.\n */\nexport function stepZoom(from: number, delta: 1 | -1): number {\n const steps = VIEWER_ZOOM_STEPS;\n const first = steps[0];\n const last = steps[steps.length - 1] as number;\n if (delta === 1) return steps.find((step) => step > from + EPSILON) ?? last;\n return [...steps].reverse().find((step) => step < from - EPSILON) ?? (first as number);\n}\n\n/** Whether there is a stop left in that direction — what disables the button. */\nexport function canStepZoom(from: number, delta: 1 | -1): boolean {\n const steps = VIEWER_ZOOM_STEPS;\n return delta === 1\n ? from < (steps[steps.length - 1] as number) - EPSILON\n : from > (steps[0] as number) + EPSILON;\n}\n","/**\n * The pdf.js seam — one place that knows the engine exists.\n *\n * Everything here is deliberately NOT React. Splitting the engine wiring from\n * the renderer keeps two things true: the dynamic `import(\"pdfjs-dist\")` has a\n * single call site (so `heavy-deps:check` has one edge to police), and an app\n * can configure the worker before any file is ever opened.\n *\n * ## Why a worker is not optional\n *\n * pdf.js parses and rasterizes on a worker thread; without one it falls back to\n * doing that on the main thread, and a 40-page document freezes the tab. The\n * default below resolves the worker that ships INSIDE `pdfjs-dist`, via\n * `new URL(…, import.meta.url)` — the form every modern bundler understands, so\n * it works with no configuration in Vite/webpack/Next.\n *\n * ## Why an app may still have to configure it\n *\n * A CSP without `worker-src blob:` — or a bundler that cannot see through the\n * `new URL` form — needs the worker served as a real asset instead. Hence\n * {@link configurePdfEngine}: set `workerSrc` to a URL your app serves, and the\n * default is never consulted. `cMapUrl` / `standardFontDataUrl` are the same\n * story for CJK text and non-embedded fonts, which pdf.js fetches on demand.\n */\n\n/** Engine wiring an app can override. Every field is optional. */\nexport interface PdfEngineConfig {\n /** URL of `pdf.worker.min.mjs`. Defaults to the copy inside `pdfjs-dist`. */\n workerSrc?: string;\n /** Directory of pdf.js `cmaps/`, for documents with CJK text. */\n cMapUrl?: string;\n /** Directory of pdf.js `standard_fonts/`, for documents that embed no fonts. */\n standardFontDataUrl?: string;\n}\n\nlet config: PdfEngineConfig = {};\n\n/**\n * Point the PDF adapter at your own copies of the pdf.js assets.\n *\n * Call once at app start, before a PDF is opened. Calling it later is harmless\n * but only affects documents opened afterwards — pdf.js reads the worker\n * setting when a document is created.\n */\nexport function configurePdfEngine(next: PdfEngineConfig): void {\n config = { ...config, ...next };\n}\n\n/** What the adapter will actually use, after any {@link configurePdfEngine} call. */\nexport function getPdfEngineConfig(): Readonly<PdfEngineConfig> {\n return config;\n}\n\n/**\n * The pdf.js surface this adapter uses, declared structurally.\n *\n * `pdfjs-dist` is an OPTIONAL peer, so its types are not guaranteed to be\n * installed — importing them would make this package's typecheck depend on a\n * package a consumer may not have. Declaring the handful of members used here\n * keeps the build honest without that coupling, and a shape change in pdf.js\n * surfaces as a runtime failure the `parse-failed` panel already handles.\n */\nexport interface PdfPageViewport {\n width: number;\n height: number;\n}\n\nexport interface PdfTextItem {\n str: string;\n transform: number[];\n width: number;\n height: number;\n fontName?: string;\n}\n\nexport interface PdfPage {\n getViewport(options: { scale: number; rotation?: number }): PdfPageViewport;\n render(options: { canvasContext: CanvasRenderingContext2D; viewport: PdfPageViewport }): {\n promise: Promise<void>;\n cancel(): void;\n };\n getTextContent(): Promise<{ items: PdfTextItem[] }>;\n cleanup(): void;\n}\n\nexport interface PdfDocument {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPage>;\n}\n\n/**\n * An open document plus the one call that releases its worker.\n *\n * The two are separate because **the worker belongs to the LOADING TASK, not to\n * the document**: pdf.js removed `PDFDocumentProxy.destroy()` in v5, so\n * `document.destroy()` is not a function on the engine this package installs. It\n * type-checked anyway — the structural `PdfDocument` interface here declared a\n * method the real proxy does not have — and threw a `TypeError` on every unmount,\n * leaking exactly the worker per document that `dispose()` exists to prevent.\n * Keeping the teardown on this object rather than on `document` is what stops the\n * mistake from being expressible.\n */\nexport interface PdfSession {\n document: PdfDocument;\n destroy(): Promise<void>;\n}\n\ninterface PdfJsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(options: Record<string, unknown>): {\n promise: Promise<PdfDocument>;\n destroy(): Promise<void>;\n };\n}\n\nlet enginePromise: Promise<PdfJsModule> | undefined;\n\n/**\n * Load pdf.js once per page load and wire its worker.\n *\n * The result is cached: opening a second PDF must not re-fetch a megabyte of\n * engine. A FAILED load is not cached, so a transient network error can be\n * retried by the viewer's own retry button.\n */\nexport async function loadPdfEngine(): Promise<PdfJsModule> {\n enginePromise ??= (async () => {\n // The ONLY edge to the optional peer. A static import here would break\n // every consumer that did not install pdfjs-dist (heavy-deps:check).\n const pdfjs = (await import(\"pdfjs-dist\")) as unknown as PdfJsModule;\n pdfjs.GlobalWorkerOptions.workerSrc = config.workerSrc ?? defaultWorkerSrc();\n return pdfjs;\n })().catch((error: unknown) => {\n enginePromise = undefined;\n throw error;\n });\n return enginePromise;\n}\n\n/**\n * The worker that ships with the installed `pdfjs-dist`.\n *\n * `new URL(…, import.meta.url)` is a *bundler instruction*, not a runtime path\n * lookup: Vite, webpack 5 and Turbopack all rewrite it to an emitted asset URL.\n * It is wrapped because a runtime without `import.meta.url` (a CJS test) throws\n * here, and a missing worker should surface as \"configure the worker\", not as an\n * unexplained crash.\n */\nfunction defaultWorkerSrc(): string {\n return new URL(\"pdfjs-dist/build/pdf.worker.min.mjs\", import.meta.url).href;\n}\n\n/** Open a document from bytes. The caller owns `destroy()` — see the adapter's `dispose`. */\nexport async function openPdfDocument(\n bytes: Uint8Array,\n signal?: AbortSignal,\n): Promise<PdfSession> {\n const pdfjs = await loadPdfEngine();\n const task = pdfjs.getDocument({\n data: bytes,\n // Defence in depth: pdf.js can evaluate font programs, and a viewer opens\n // files the app did not write. The cost is a rare, slightly slower font\n // path; the benefit is that a malicious PDF has no `eval` to reach for.\n isEvalSupported: false,\n cMapUrl: config.cMapUrl,\n cMapPacked: true,\n standardFontDataUrl: config.standardFontDataUrl,\n });\n signal?.addEventListener(\"abort\", () => void task.destroy(), { once: true });\n const document = await task.promise;\n return { document, destroy: () => task.destroy() };\n}\n"],"mappings":";;;;;;AAiBO,IAAM,cAA+B;AAAA,EAC1C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,KAAK;AAAA,EAClB,YAAY,CAAC,iBAAiB;AAAA,EAC9B,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW,CAAC,SAAS,SAAS,MAAM;AAAA,EACtC;AAAA,EACA,UAAU,CAAC,YAAY;AACzB;;;ACTO,IAAM,oBAAoB,CAAC,KAAK,MAAM,GAAG,MAAM,KAAK,GAAG,CAAC;AAGxD,IAAM,eAAe;AAG5B,IAAM,UAAU;AAGT,SAAS,UAAU,MAAkC;AAC1D,SAAO,OAAO,SAAS;AACzB;AAUO,SAAS,SAAS,MAAc,OAAuB;AAC5D,QAAM,QAAQ;AACd,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,MAAI,UAAU,EAAG,QAAO,MAAM,KAAK,CAAC,SAAS,OAAO,OAAO,OAAO,KAAK;AACvE,SAAO,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,OAAO,OAAO,OAAO,KAAM;AACxE;AAGO,SAAS,YAAY,MAAc,OAAwB;AAChE,QAAM,QAAQ;AACd,SAAO,UAAU,IACb,OAAQ,MAAM,MAAM,SAAS,CAAC,IAAe,UAC7C,OAAQ,MAAM,CAAC,IAAe;AACpC;;;ACpBA,IAAI,SAA0B,CAAC;AASxB,SAAS,mBAAmB,MAA6B;AAC9D,WAAS,EAAE,GAAG,QAAQ,GAAG,KAAK;AAChC;AAGO,SAAS,qBAAgD;AAC9D,SAAO;AACT;AAgEA,IAAI;AASJ,eAAsB,gBAAsC;AAC1D,qBAAmB,YAAY;AAG7B,UAAM,QAAS,MAAM,OAAO,YAAY;AACxC,UAAM,oBAAoB,YAAY,OAAO,aAAa,iBAAiB;AAC3E,WAAO;AAAA,EACT,GAAG,EAAE,MAAM,CAAC,UAAmB;AAC7B,oBAAgB;AAChB,UAAM;AAAA,EACR,CAAC;AACD,SAAO;AACT;AAWA,SAAS,mBAA2B;AAClC,SAAO,IAAI,IAAI,uCAAuC,YAAY,GAAG,EAAE;AACzE;AAGA,eAAsB,gBACpB,OACA,QACqB;AACrB,QAAM,QAAQ,MAAM,cAAc;AAClC,QAAM,OAAO,MAAM,YAAY;AAAA,IAC7B,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,iBAAiB;AAAA,IACjB,SAAS,OAAO;AAAA,IAChB,YAAY;AAAA,IACZ,qBAAqB,OAAO;AAAA,EAC9B,CAAC;AACD,UAAQ,iBAAiB,SAAS,MAAM,KAAK,KAAK,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC;AAC3E,QAAM,WAAW,MAAM,KAAK;AAC5B,SAAO,EAAE,UAAU,SAAS,MAAM,KAAK,QAAQ,EAAE;AACnD;","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_VERSION
|
|
4
|
+
} from "./chunk-AORNMH77.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/text/text-manifest.ts
|
|
7
|
+
var textManifest = {
|
|
8
|
+
id: "text",
|
|
9
|
+
protocol: PROTOCOL_VERSION,
|
|
10
|
+
categories: ["text", "code", "data", "unknown"],
|
|
11
|
+
mediaTypes: ["text/"],
|
|
12
|
+
// `rect` is absent on purpose: a plain-text file has no geometry to point at,
|
|
13
|
+
// and a manifest that claimed it would let an app offer an affordance the
|
|
14
|
+
// renderer can never honour.
|
|
15
|
+
capabilities: { text: true, search: true, highlight: ["quote", "range"] }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
textManifest
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=chunk-UJIJECEF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/text/text-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only (see `image-manifest.ts`).\n *\n * Claims only broad CATEGORIES, so any adapter naming an extension or an exact\n * MIME outranks it automatically — this is the \"readable as text\" backstop.\n */\nexport const textManifest: AdapterManifest = {\n id: \"text\",\n protocol: PROTOCOL_VERSION,\n categories: [\"text\", \"code\", \"data\", \"unknown\"],\n mediaTypes: [\"text/\"],\n // `rect` is absent on purpose: a plain-text file has no geometry to point at,\n // and a manifest that claimed it would let an app offer an affordance the\n // renderer can never honour.\n capabilities: { text: true, search: true, highlight: [\"quote\", \"range\"] },\n};\n"],"mappings":";;;;;;AASO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,QAAQ,QAAQ,QAAQ,SAAS;AAAA,EAC9C,YAAY,CAAC,OAAO;AAAA;AAAA;AAAA;AAAA,EAIpB,cAAc,EAAE,MAAM,MAAM,QAAQ,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAC1E;","names":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/core/text-index.ts
|
|
4
|
+
function createTextIndexBuilder(options = {}) {
|
|
5
|
+
const fallback = options.separator ?? "\n";
|
|
6
|
+
const parts = [];
|
|
7
|
+
const spans = [];
|
|
8
|
+
let cursor = 0;
|
|
9
|
+
return {
|
|
10
|
+
push(chunk, ref, separator = fallback) {
|
|
11
|
+
if (chunk.length === 0) return;
|
|
12
|
+
if (parts.length > 0) {
|
|
13
|
+
parts.push(separator);
|
|
14
|
+
cursor += separator.length;
|
|
15
|
+
}
|
|
16
|
+
parts.push(chunk);
|
|
17
|
+
spans.push({ start: cursor, end: cursor + chunk.length, ref });
|
|
18
|
+
cursor += chunk.length;
|
|
19
|
+
},
|
|
20
|
+
build() {
|
|
21
|
+
return { text: parts.join(""), spans };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function lowerBound(spans, offset) {
|
|
26
|
+
let lo = 0;
|
|
27
|
+
let hi = spans.length;
|
|
28
|
+
while (lo < hi) {
|
|
29
|
+
const mid = lo + hi >> 1;
|
|
30
|
+
if (spans[mid].end <= offset) lo = mid + 1;
|
|
31
|
+
else hi = mid;
|
|
32
|
+
}
|
|
33
|
+
return lo;
|
|
34
|
+
}
|
|
35
|
+
function spansForRange(index, [start, end]) {
|
|
36
|
+
if (end <= start) return [];
|
|
37
|
+
const out = [];
|
|
38
|
+
for (let i = lowerBound(index.spans, start); i < index.spans.length; i += 1) {
|
|
39
|
+
const span = index.spans[i];
|
|
40
|
+
if (span.start >= end) break;
|
|
41
|
+
out.push({
|
|
42
|
+
span,
|
|
43
|
+
start: Math.max(start, span.start) - span.start,
|
|
44
|
+
end: Math.min(end, span.end) - span.start
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function chunkOffset(parts, index, start, separator) {
|
|
50
|
+
if (start === void 0) return void 0;
|
|
51
|
+
let offset = start;
|
|
52
|
+
for (let i = 0; i < index; i += 1) {
|
|
53
|
+
offset += (parts[i]?.length ?? 0) + separator.length;
|
|
54
|
+
}
|
|
55
|
+
return offset;
|
|
56
|
+
}
|
|
57
|
+
function spanAt(index, offset) {
|
|
58
|
+
const span = index.spans[lowerBound(index.spans, offset)];
|
|
59
|
+
return span && span.start <= offset ? span : void 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
createTextIndexBuilder,
|
|
64
|
+
spansForRange,
|
|
65
|
+
chunkOffset,
|
|
66
|
+
spanAt
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=chunk-UL43NGUG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/text-index.ts"],"sourcesContent":["/**\n * The map from a document's flat text projection back into its own model.\n *\n * `AdapterDocument.text` is the address space every text-based highlight is\n * expressed in — but a renderer does not draw a flat string. It draws Word\n * blocks, PDF pages, spreadsheet cells, code lines. So \"characters 812–847 of\n * the projection\" has to become \"the second half of block 14\" before anything\n * can be painted, and only the adapter knows how.\n *\n * That translation is the same bookkeeping every time — running offsets, a\n * separator between chunks, a lookup by position — so it is written once here\n * and each adapter supplies only its own `ref` type: a block index, a page\n * number, a `{ sheet, row, column }` triple. The builder is what guarantees the\n * projection and the index can never disagree, because the projection is its\n * output rather than a second thing assembled by hand.\n */\n\nimport type { MatchRange } from \"@elabs-ai/components-ui\";\n\n/** One chunk of the projection, and what it came from. */\nexport interface TextSpan<TRef> {\n /** Half-open `[start, end)` into the projection's text. */\n start: number;\n end: number;\n /** The adapter's own pointer back into its model. */\n ref: TRef;\n}\n\n/** A flat text projection plus the way back into the model that produced it. */\nexport interface TextIndex<TRef> {\n text: string;\n /** Sorted by `start`, non-overlapping. Gaps are the separators between chunks. */\n spans: readonly TextSpan<TRef>[];\n}\n\nexport interface TextIndexBuilderOptions {\n /**\n * Written between chunks and owned by no span, so a highlight can never be\n * reported as covering the joint between two blocks. `\"\\n\"` by default.\n */\n separator?: string;\n}\n\nexport interface TextIndexBuilder<TRef> {\n /**\n * Append a chunk. Empty chunks are skipped — an empty span matches nothing.\n *\n * `separator` overrides the builder's default for THIS joint only, which is\n * how a multi-level projection stays one index: a workbook joins cells with a\n * tab, rows with a newline and sheets with a blank line, and stitching three\n * separate indexes together afterwards is exactly the offset bookkeeping this\n * module exists to do once.\n */\n push(chunk: string, ref: TRef, separator?: string): void;\n build(): TextIndex<TRef>;\n}\n\n/** Assemble a projection and its index together, so they cannot drift apart. */\nexport function createTextIndexBuilder<TRef>(\n options: TextIndexBuilderOptions = {},\n): TextIndexBuilder<TRef> {\n const fallback = options.separator ?? \"\\n\";\n const parts: string[] = [];\n const spans: TextSpan<TRef>[] = [];\n let cursor = 0;\n\n return {\n push(chunk, ref, separator = fallback) {\n if (chunk.length === 0) return;\n if (parts.length > 0) {\n parts.push(separator);\n cursor += separator.length;\n }\n parts.push(chunk);\n spans.push({ start: cursor, end: cursor + chunk.length, ref });\n cursor += chunk.length;\n },\n build() {\n return { text: parts.join(\"\"), spans };\n },\n };\n}\n\n/** A span a range touches, with the overlap expressed in the CHUNK's own offsets. */\nexport interface SpanOverlap<TRef> {\n span: TextSpan<TRef>;\n /** Offset into the chunk, not into the projection. */\n start: number;\n end: number;\n}\n\n/** Index of the first span ending after `offset`, or `spans.length`. */\nfunction lowerBound<TRef>(spans: readonly TextSpan<TRef>[], offset: number): number {\n let lo = 0;\n let hi = spans.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if ((spans[mid] as TextSpan<TRef>).end <= offset) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\n\n/**\n * Every chunk a projection range touches, clipped to that chunk.\n *\n * Binary search rather than a scan: a highlighted sentence in a 5,000-cell\n * spreadsheet touches one or two spans, and walking all 5,000 to find them —\n * once per highlight, on every render — is what makes a find-as-you-type box\n * stutter.\n */\nexport function spansForRange<TRef>(\n index: TextIndex<TRef>,\n [start, end]: MatchRange,\n): SpanOverlap<TRef>[] {\n if (end <= start) return [];\n const out: SpanOverlap<TRef>[] = [];\n for (let i = lowerBound(index.spans, start); i < index.spans.length; i += 1) {\n const span = index.spans[i] as TextSpan<TRef>;\n if (span.start >= end) break;\n out.push({\n span,\n start: Math.max(start, span.start) - span.start,\n end: Math.min(end, span.end) - span.start,\n });\n }\n return out;\n}\n\n/**\n * Where the `index`-th part of a joined string begins, given where the join\n * begins.\n *\n * The counterpart of pushing a whole row as one chunk: the REF can be the row\n * while the MARK is still the character, because a cell's offset is the sum of\n * the cells before it plus their separators. Used by every renderer that draws\n * one chunk in several pieces — a Word table row, a spreadsheet row, a\n * paragraph's styled runs. `undefined` in, `undefined` out, so a caller with no\n * index does not need a branch.\n */\nexport function chunkOffset(\n parts: readonly string[],\n index: number,\n start: number | undefined,\n separator: string,\n): number | undefined {\n if (start === undefined) return undefined;\n let offset = start;\n for (let i = 0; i < index; i += 1) {\n offset += (parts[i]?.length ?? 0) + separator.length;\n }\n return offset;\n}\n\n/** The chunk containing `offset`, or `undefined` when it lands on a separator. */\nexport function spanAt<TRef>(index: TextIndex<TRef>, offset: number): TextSpan<TRef> | undefined {\n const span = index.spans[lowerBound(index.spans, offset)];\n return span && span.start <= offset ? span : undefined;\n}\n"],"mappings":";;;AA0DO,SAAS,uBACd,UAAmC,CAAC,GACZ;AACxB,QAAM,WAAW,QAAQ,aAAa;AACtC,QAAM,QAAkB,CAAC;AACzB,QAAM,QAA0B,CAAC;AACjC,MAAI,SAAS;AAEb,SAAO;AAAA,IACL,KAAK,OAAO,KAAK,YAAY,UAAU;AACrC,UAAI,MAAM,WAAW,EAAG;AACxB,UAAI,MAAM,SAAS,GAAG;AACpB,cAAM,KAAK,SAAS;AACpB,kBAAU,UAAU;AAAA,MACtB;AACA,YAAM,KAAK,KAAK;AAChB,YAAM,KAAK,EAAE,OAAO,QAAQ,KAAK,SAAS,MAAM,QAAQ,IAAI,CAAC;AAC7D,gBAAU,MAAM;AAAA,IAClB;AAAA,IACA,QAAQ;AACN,aAAO,EAAE,MAAM,MAAM,KAAK,EAAE,GAAG,MAAM;AAAA,IACvC;AAAA,EACF;AACF;AAWA,SAAS,WAAiB,OAAkC,QAAwB;AAClF,MAAI,KAAK;AACT,MAAI,KAAK,MAAM;AACf,SAAO,KAAK,IAAI;AACd,UAAM,MAAO,KAAK,MAAO;AACzB,QAAK,MAAM,GAAG,EAAqB,OAAO,OAAQ,MAAK,MAAM;AAAA,QACxD,MAAK;AAAA,EACZ;AACA,SAAO;AACT;AAUO,SAAS,cACd,OACA,CAAC,OAAO,GAAG,GACU;AACrB,MAAI,OAAO,MAAO,QAAO,CAAC;AAC1B,QAAM,MAA2B,CAAC;AAClC,WAAS,IAAI,WAAW,MAAM,OAAO,KAAK,GAAG,IAAI,MAAM,MAAM,QAAQ,KAAK,GAAG;AAC3E,UAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,QAAI,KAAK,SAAS,IAAK;AACvB,QAAI,KAAK;AAAA,MACP;AAAA,MACA,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK,IAAI,KAAK;AAAA,MAC1C,KAAK,KAAK,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK;AAAA,IACtC,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAaO,SAAS,YACd,OACA,OACA,OACA,WACoB;AACpB,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;AACjC,eAAW,MAAM,CAAC,GAAG,UAAU,KAAK,UAAU;AAAA,EAChD;AACA,SAAO;AACT;AAGO,SAAS,OAAa,OAAwB,QAA4C;AAC/F,QAAM,OAAO,MAAM,MAAM,WAAW,MAAM,OAAO,MAAM,CAAC;AACxD,SAAO,QAAQ,KAAK,SAAS,SAAS,OAAO;AAC/C;","names":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/core/highlight-marks.ts
|
|
4
|
+
import { normalizeRanges } from "@elabs-ai/components-ui";
|
|
5
|
+
var EMPTY = { ranges: [], activeIndex: -1 };
|
|
6
|
+
function toMarkRanges(highlights, textLength) {
|
|
7
|
+
if (!highlights || highlights.length === 0) return EMPTY;
|
|
8
|
+
const located = highlights.filter(
|
|
9
|
+
(highlight) => highlight.status === "resolved" && highlight.range !== void 0
|
|
10
|
+
);
|
|
11
|
+
if (located.length === 0) return EMPTY;
|
|
12
|
+
const ranges = normalizeRanges(
|
|
13
|
+
located.map((highlight) => highlight.range),
|
|
14
|
+
textLength
|
|
15
|
+
);
|
|
16
|
+
const active = located.find((highlight) => highlight.active);
|
|
17
|
+
const activeStart = active?.range[0];
|
|
18
|
+
const activeIndex = activeStart === void 0 ? -1 : ranges.findIndex(([start, end]) => activeStart >= start && activeStart < end);
|
|
19
|
+
return { ranges, activeIndex };
|
|
20
|
+
}
|
|
21
|
+
function localizeRanges({ ranges, activeIndex }, start, end) {
|
|
22
|
+
if (ranges.length === 0 || start >= end) return EMPTY;
|
|
23
|
+
const local = [];
|
|
24
|
+
let localActive = -1;
|
|
25
|
+
ranges.forEach(([from, to], index) => {
|
|
26
|
+
const clippedFrom = Math.max(from, start);
|
|
27
|
+
const clippedTo = Math.min(to, end);
|
|
28
|
+
if (clippedFrom >= clippedTo) return;
|
|
29
|
+
if (index === activeIndex) localActive = local.length;
|
|
30
|
+
local.push([clippedFrom - start, clippedTo - start]);
|
|
31
|
+
});
|
|
32
|
+
return local.length === 0 ? EMPTY : { ranges: local, activeIndex: localActive };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/core/use-highlight-scroll.ts
|
|
36
|
+
import { useEffect } from "react";
|
|
37
|
+
import { useReducedMotion } from "@elabs-ai/components-tokens";
|
|
38
|
+
var ACTIVE_HIGHLIGHT_SELECTOR = '[data-slot="match-highlight-mark"][data-active],[data-slot="highlight-rect"][data-active],[data-slot="highlight-block"][data-active]';
|
|
39
|
+
function useScrollActiveHighlightIntoView(containerRef, activeHighlightId, paintKey) {
|
|
40
|
+
const reducedMotion = useReducedMotion();
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!activeHighlightId) return;
|
|
43
|
+
const target = containerRef.current?.querySelector(ACTIVE_HIGHLIGHT_SELECTOR);
|
|
44
|
+
if (!(target instanceof HTMLElement) || typeof target.scrollIntoView !== "function") return;
|
|
45
|
+
target.scrollIntoView({ block: "center", behavior: reducedMotion ? "auto" : "smooth" });
|
|
46
|
+
}, [containerRef, activeHighlightId, paintKey, reducedMotion]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
toMarkRanges,
|
|
51
|
+
localizeRanges,
|
|
52
|
+
ACTIVE_HIGHLIGHT_SELECTOR,
|
|
53
|
+
useScrollActiveHighlightIntoView
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-VO2273Z2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/highlight-marks.ts","../src/core/use-highlight-scroll.ts"],"sourcesContent":["/**\n * The MAP + PAINT half of the highlight funnel, for adapters whose document IS\n * its own text projection — plain text, code, and anything else that renders\n * `document.text` verbatim.\n *\n * The shell has already done LOCATE (every quote is now a character range). All\n * that is left is to hand those ranges to `MatchHighlight` and say which one is\n * current. An adapter with a richer model (pages, blocks, cells) maps the same\n * ranges through its own `textIndex` instead and does not use this.\n */\n\nimport { normalizeRanges, type MatchRange } from \"@elabs-ai/components-ui\";\n\nimport type { ResolvedHighlight } from \"./highlight\";\n\n/** What `MatchHighlight` takes: merged ranges, plus which merged mark is current. */\nexport interface MarkRanges {\n /** Clamped, sorted and merged — the same ranges `MatchHighlight` will paint. */\n readonly ranges: readonly MatchRange[];\n /** Index into {@link MarkRanges.ranges}, or `-1` when none is current. */\n readonly activeIndex: number;\n}\n\nconst EMPTY: MarkRanges = { ranges: [], activeIndex: -1 };\n\n/**\n * Fold resolved highlights into the ranges a `<mark>` layer paints.\n *\n * Merging happens HERE rather than being left to `MatchHighlight`, because the\n * two would otherwise disagree about what \"the third mark\" is: overlapping\n * citations collapse into one mark, and an `activeIndex` counted against the\n * unmerged list would then point at the wrong one — or past the end. The active\n * entry is located by which merged range CONTAINS its start, so it stays right\n * no matter how many requests collapsed into that mark.\n *\n * Non-`range` addresses and unresolved entries are skipped: an adapter paints\n * what it can and the shell is what tells the reader about the rest.\n */\nexport function toMarkRanges(\n highlights: readonly ResolvedHighlight[] | undefined,\n textLength: number,\n): MarkRanges {\n if (!highlights || highlights.length === 0) return EMPTY;\n\n const located = highlights.filter(\n (highlight): highlight is ResolvedHighlight & { range: MatchRange } =>\n highlight.status === \"resolved\" && highlight.range !== undefined,\n );\n if (located.length === 0) return EMPTY;\n\n const ranges = normalizeRanges(\n located.map((highlight) => highlight.range),\n textLength,\n );\n\n const active = located.find((highlight) => highlight.active);\n const activeStart = active?.range[0];\n const activeIndex =\n activeStart === undefined\n ? -1\n : ranges.findIndex(([start, end]) => activeStart >= start && activeStart < end);\n\n return { ranges, activeIndex };\n}\n\n/**\n * Re-express document ranges relative to one slice of the document — a code\n * token, a table cell, a block of prose.\n *\n * A renderer that draws its text in pieces cannot hand whole-document offsets to\n * a `<mark>` layer that only knows about the piece in front of it. Ranges are\n * clipped to the slice and rebased to 0; the ones that miss it entirely drop\n * out, which is also how a caller learns there is nothing to mark here (an empty\n * `ranges`). A range spanning several slices survives in each of them, so a\n * citation crossing a line break stays one visual run.\n */\nexport function localizeRanges(\n { ranges, activeIndex }: MarkRanges,\n start: number,\n end: number,\n): MarkRanges {\n if (ranges.length === 0 || start >= end) return EMPTY;\n\n const local: MatchRange[] = [];\n let localActive = -1;\n ranges.forEach(([from, to], index) => {\n const clippedFrom = Math.max(from, start);\n const clippedTo = Math.min(to, end);\n if (clippedFrom >= clippedTo) return;\n if (index === activeIndex) localActive = local.length;\n local.push([clippedFrom - start, clippedTo - start]);\n });\n\n return local.length === 0 ? EMPTY : { ranges: local, activeIndex: localActive };\n}\n","\"use client\";\n\n/**\n * Bring the current highlight into view.\n *\n * Pointing at a passage is only half the job — a mark 4,000 lines down that\n * nobody scrolls to is the same as no mark at all. Every text-ish renderer needs\n * this, so it is written once here rather than per adapter.\n *\n * The current highlight is found by the stable `data-slot` selector its painter\n * emits, not by a ref: both painters produce their elements from a list — marks\n * inside `MatchHighlight`, boxes inside a page overlay — so the renderer has no\n * handle on the individual element and should not grow one just for scrolling.\n */\n\nimport { useEffect, type RefObject } from \"react\";\n\nimport { useReducedMotion } from \"@elabs-ai/components-tokens\";\n\n/**\n * How the current highlight identifies itself in the DOM, whichever way it was\n * painted: a `<mark>` in flowing text, a box over a page raster, or a whole\n * block plated because the format can only be addressed that coarsely.\n */\nexport const ACTIVE_HIGHLIGHT_SELECTOR =\n '[data-slot=\"match-highlight-mark\"][data-active],[data-slot=\"highlight-rect\"][data-active],' +\n '[data-slot=\"highlight-block\"][data-active]';\n\n/**\n * Scroll the current highlight inside `containerRef` into view whenever the\n * viewer is pointed somewhere new.\n *\n * `block: \"center\"` rather than `\"nearest\"`: a cited passage should land where a\n * reader looks, with its surrounding context visible, not flush against the top\n * edge of the pane. Smooth scrolling is skipped under a reduced-motion\n * preference — an imperative scroll is invisible to the CSS media query, so the\n * check has to be explicit.\n *\n * `paintKey` is anything whose change means the highlights were repainted — a\n * page's box list, a re-tokenized document. A renderer that paints\n * asynchronously (a PDF fetches the page's text after the raster) has no element\n * to scroll to at the moment the active id changes, so it passes the thing it\n * was waiting for and the scroll runs once that arrives.\n */\nexport function useScrollActiveHighlightIntoView(\n containerRef: RefObject<HTMLElement | null>,\n activeHighlightId: string | null | undefined,\n paintKey?: unknown,\n): void {\n const reducedMotion = useReducedMotion();\n\n useEffect(() => {\n if (!activeHighlightId) return;\n const target = containerRef.current?.querySelector(ACTIVE_HIGHLIGHT_SELECTOR);\n // jsdom ships no `scrollIntoView`; a viewer must not throw in a test run.\n if (!(target instanceof HTMLElement) || typeof target.scrollIntoView !== \"function\") return;\n target.scrollIntoView({ block: \"center\", behavior: reducedMotion ? \"auto\" : \"smooth\" });\n }, [containerRef, activeHighlightId, paintKey, reducedMotion]);\n}\n"],"mappings":";;;AAWA,SAAS,uBAAwC;AAYjD,IAAM,QAAoB,EAAE,QAAQ,CAAC,GAAG,aAAa,GAAG;AAejD,SAAS,aACd,YACA,YACY;AACZ,MAAI,CAAC,cAAc,WAAW,WAAW,EAAG,QAAO;AAEnD,QAAM,UAAU,WAAW;AAAA,IACzB,CAAC,cACC,UAAU,WAAW,cAAc,UAAU,UAAU;AAAA,EAC3D;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,SAAS;AAAA,IACb,QAAQ,IAAI,CAAC,cAAc,UAAU,KAAK;AAAA,IAC1C;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,KAAK,CAAC,cAAc,UAAU,MAAM;AAC3D,QAAM,cAAc,QAAQ,MAAM,CAAC;AACnC,QAAM,cACJ,gBAAgB,SACZ,KACA,OAAO,UAAU,CAAC,CAAC,OAAO,GAAG,MAAM,eAAe,SAAS,cAAc,GAAG;AAElF,SAAO,EAAE,QAAQ,YAAY;AAC/B;AAaO,SAAS,eACd,EAAE,QAAQ,YAAY,GACtB,OACA,KACY;AACZ,MAAI,OAAO,WAAW,KAAK,SAAS,IAAK,QAAO;AAEhD,QAAM,QAAsB,CAAC;AAC7B,MAAI,cAAc;AAClB,SAAO,QAAQ,CAAC,CAAC,MAAM,EAAE,GAAG,UAAU;AACpC,UAAM,cAAc,KAAK,IAAI,MAAM,KAAK;AACxC,UAAM,YAAY,KAAK,IAAI,IAAI,GAAG;AAClC,QAAI,eAAe,UAAW;AAC9B,QAAI,UAAU,YAAa,eAAc,MAAM;AAC/C,UAAM,KAAK,CAAC,cAAc,OAAO,YAAY,KAAK,CAAC;AAAA,EACrD,CAAC;AAED,SAAO,MAAM,WAAW,IAAI,QAAQ,EAAE,QAAQ,OAAO,aAAa,YAAY;AAChF;;;AC/EA,SAAS,iBAAiC;AAE1C,SAAS,wBAAwB;AAO1B,IAAM,4BACX;AAmBK,SAAS,iCACd,cACA,mBACA,UACM;AACN,QAAM,gBAAgB,iBAAiB;AAEvC,YAAU,MAAM;AACd,QAAI,CAAC,kBAAmB;AACxB,UAAM,SAAS,aAAa,SAAS,cAAc,yBAAyB;AAE5E,QAAI,EAAE,kBAAkB,gBAAgB,OAAO,OAAO,mBAAmB,WAAY;AACrF,WAAO,eAAe,EAAE,OAAO,UAAU,UAAU,gBAAgB,SAAS,SAAS,CAAC;AAAA,EACxF,GAAG,CAAC,cAAc,mBAAmB,UAAU,aAAa,CAAC;AAC/D;","names":[]}
|