@gmickel/gno 2.8.1 → 2.8.3
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 +1 -1
- package/assets/skill/SKILL.md +4 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v2.8.1.zip → gno-browser-clipper-v2.8.3.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.3.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +9 -1
- package/spec/cli.md +81 -8
- package/spec/mcp.md +5 -6
- package/src/cli/commands/daemon.ts +7 -2
- package/src/cli/commands/doctor.ts +42 -24
- package/src/cli/commands/index-cmd.ts +10 -2
- package/src/cli/commands/links.ts +5 -22
- package/src/cli/commands/shared.ts +26 -1
- package/src/cli/commands/status.ts +60 -0
- package/src/cli/commands/update.ts +6 -1
- package/src/cli/commands/vec.ts +42 -2
- package/src/cli/program.ts +2 -2
- package/src/config/types.ts +19 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -52
- package/src/converters/adapters/shared/ooxml-protection.ts +58 -0
- package/src/converters/adapters/xlsx/adapter.ts +132 -0
- package/src/converters/adapters/xlsx/sheet-markdown.ts +312 -0
- package/src/converters/native/markdown.ts +3 -1
- package/src/converters/registry.ts +62 -6
- package/src/converters/versions.ts +2 -0
- package/src/core/audit-freshness.ts +1 -1
- package/src/core/audit-links.ts +2 -1
- package/src/core/audit-outside-index.ts +91 -18
- package/src/core/change-diff.ts +18 -6
- package/src/core/link-destination-parse.ts +21 -4
- package/src/core/link-inventory-markdown.ts +23 -3
- package/src/core/link-inventory-opaque.ts +3 -1
- package/src/core/links.ts +74 -16
- package/src/core/network-boundary-inventory.ts +18 -0
- package/src/index.ts +10 -0
- package/src/ingestion/document-metadata.ts +256 -0
- package/src/ingestion/file-child-env.ts +9 -0
- package/src/ingestion/file-child.ts +106 -0
- package/src/ingestion/file-processor.ts +546 -0
- package/src/ingestion/frontmatter.ts +6 -2
- package/src/ingestion/index.ts +1 -0
- package/src/ingestion/prepare-convert.ts +32 -0
- package/src/ingestion/prepare-file.ts +272 -0
- package/src/ingestion/prepare-worker.ts +43 -0
- package/src/ingestion/record-container.ts +7 -2
- package/src/ingestion/strip.ts +149 -133
- package/src/ingestion/sync-options.ts +7 -1
- package/src/ingestion/sync.ts +208 -385
- package/src/ingestion/types.ts +18 -0
- package/src/llm/native-worker/embedding-identity.ts +15 -3
- package/src/mcp/http-transport.ts +11 -0
- package/src/mcp/tools/links.ts +10 -38
- package/src/serve/resident-request.ts +19 -0
- package/src/serve/resident-runtime.ts +84 -8
- package/src/serve/routes/links.ts +6 -24
- package/src/serve/server.ts +3 -2
- package/src/sessions/config-refresh.ts +3 -2
- package/src/store/sqlite/graph-similarity.ts +5 -13
- package/src/store/vector/partition-index.ts +100 -0
- package/src/store/vector/stored-vectors.ts +20 -15
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +0 -1
package/src/cli/commands/vec.ts
CHANGED
|
@@ -13,10 +13,16 @@ import {
|
|
|
13
13
|
createVectorIndexPort,
|
|
14
14
|
createVectorStatsPort,
|
|
15
15
|
} from "../../store/vector";
|
|
16
|
+
import {
|
|
17
|
+
rebuildPartitionIndex,
|
|
18
|
+
syncPartitionIndex,
|
|
19
|
+
} from "../../store/vector/partition-index";
|
|
16
20
|
import {
|
|
17
21
|
dropVectorPartition,
|
|
22
|
+
storedVectorPartition,
|
|
18
23
|
type VectorPartitionStatus,
|
|
19
24
|
} from "../../store/vector/status";
|
|
25
|
+
import { loadSqliteVec } from "../../store/vector/variants";
|
|
20
26
|
|
|
21
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
28
|
// Types
|
|
@@ -76,7 +82,9 @@ function inferDimensions(
|
|
|
76
82
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
83
|
|
|
78
84
|
/**
|
|
79
|
-
* Sync vec0 index with
|
|
85
|
+
* Sync the vec0 index with stored vectors (add missing, remove orphans): the
|
|
86
|
+
* active vector partition's variants, or legacy `content_vectors` on an
|
|
87
|
+
* index that never activated a partition.
|
|
80
88
|
*/
|
|
81
89
|
export async function vecSync(
|
|
82
90
|
options: VecOptions = {}
|
|
@@ -112,6 +120,21 @@ export async function vecSync(
|
|
|
112
120
|
try {
|
|
113
121
|
const db = store.getRawDb();
|
|
114
122
|
|
|
123
|
+
const partition = storedVectorPartition(db, modelUri);
|
|
124
|
+
if (partition) {
|
|
125
|
+
if (!(await loadSqliteVec(db))) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: "sqlite-vec not available. Cannot sync index.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
...syncPartitionIndex(db, partition),
|
|
134
|
+
model: modelUri,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
// Infer dimensions from stored vectors for this model
|
|
116
139
|
const dimensions = inferDimensions(db, modelUri);
|
|
117
140
|
if (dimensions === null) {
|
|
@@ -154,7 +177,9 @@ export async function vecSync(
|
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/**
|
|
157
|
-
* Rebuild vec0 index
|
|
180
|
+
* Rebuild the vec0 index (drop + recreate + repopulate) from the active
|
|
181
|
+
* vector partition's variants, or from legacy `content_vectors` on an index
|
|
182
|
+
* that never activated a partition.
|
|
158
183
|
*/
|
|
159
184
|
export async function vecRebuild(
|
|
160
185
|
options: VecOptions = {}
|
|
@@ -190,6 +215,21 @@ export async function vecRebuild(
|
|
|
190
215
|
try {
|
|
191
216
|
const db = store.getRawDb();
|
|
192
217
|
|
|
218
|
+
const partition = storedVectorPartition(db, modelUri);
|
|
219
|
+
if (partition) {
|
|
220
|
+
if (!(await loadSqliteVec(db))) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error: "sqlite-vec not available. Cannot rebuild index.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
count: rebuildPartitionIndex(db, partition),
|
|
229
|
+
model: modelUri,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
193
233
|
// Infer dimensions from stored vectors for this model
|
|
194
234
|
const dimensions = inferDimensions(db, modelUri);
|
|
195
235
|
if (dimensions === null) {
|
package/src/cli/program.ts
CHANGED
|
@@ -3655,7 +3655,7 @@ function wireVecCommands(program: Command): void {
|
|
|
3655
3655
|
addWriteLeaseFlags(
|
|
3656
3656
|
vecCmd
|
|
3657
3657
|
.command("sync")
|
|
3658
|
-
.description("Sync vec0 index with
|
|
3658
|
+
.description("Sync the vec0 index with the active vector partition")
|
|
3659
3659
|
.option("--json", "JSON output")
|
|
3660
3660
|
).action(async (cmdOpts: Record<string, unknown>) => {
|
|
3661
3661
|
const format = getFormat(cmdOpts);
|
|
@@ -3728,7 +3728,7 @@ function wireVecCommands(program: Command): void {
|
|
|
3728
3728
|
addWriteLeaseFlags(
|
|
3729
3729
|
vecCmd
|
|
3730
3730
|
.command("rebuild")
|
|
3731
|
-
.description("Rebuild vec0 index from
|
|
3731
|
+
.description("Rebuild the vec0 index from the active vector partition")
|
|
3732
3732
|
.option("--json", "JSON output")
|
|
3733
3733
|
).action(async (cmdOpts: Record<string, unknown>) => {
|
|
3734
3734
|
const format = getFormat(cmdOpts);
|
package/src/config/types.ts
CHANGED
|
@@ -429,6 +429,22 @@ export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
|
|
|
429
429
|
},
|
|
430
430
|
];
|
|
431
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Per-file conversion budget. A file that overruns either limit is recorded
|
|
434
|
+
* with TIMEOUT or MEMORY_LIMIT and retried by the next update.
|
|
435
|
+
*/
|
|
436
|
+
export const ConversionConfigSchema = z
|
|
437
|
+
.object({
|
|
438
|
+
/** Wall-clock budget for converting one file, in ms (default 60000). */
|
|
439
|
+
timeoutMs: z.number().int().min(1000).max(86_400_000).optional(),
|
|
440
|
+
/**
|
|
441
|
+
* Resident-memory ceiling while a file converts, in MB (default: half
|
|
442
|
+
* of physical memory, at least 2048).
|
|
443
|
+
*/
|
|
444
|
+
maxMemoryMb: z.number().int().min(256).max(1_048_576).optional(),
|
|
445
|
+
})
|
|
446
|
+
.strict();
|
|
447
|
+
|
|
432
448
|
export const ModelConfigSchema = z.object({
|
|
433
449
|
/** Active preset ID */
|
|
434
450
|
activePreset: z.string().default("slim-tuned"),
|
|
@@ -608,6 +624,9 @@ export const ConfigSchema = z.object({
|
|
|
608
624
|
/** Optional index-wide chunk size/overlap; omitted preserves legacy defaults. */
|
|
609
625
|
chunking: ChunkingConfigSchema.optional(),
|
|
610
626
|
|
|
627
|
+
/** Per-file conversion budget; omitted fields use the runtime defaults. */
|
|
628
|
+
conversion: ConversionConfigSchema.optional(),
|
|
629
|
+
|
|
611
630
|
/** Model configuration */
|
|
612
631
|
models: ModelConfigSchema.optional(),
|
|
613
632
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
tooLargeError,
|
|
20
20
|
} from "../../errors";
|
|
21
21
|
import { ADAPTER_VERSIONS } from "../../versions";
|
|
22
|
+
import { hasPrefix, isPasswordProtectedXlsx } from "../shared/ooxml-protection";
|
|
22
23
|
|
|
23
24
|
const CONVERTER_ID = "adapter/markitdown-ts" as const;
|
|
24
25
|
const CONVERTER_VERSION = ADAPTER_VERSIONS["markitdown-ts"];
|
|
@@ -34,11 +35,6 @@ const SUPPORTED_MIMES = [
|
|
|
34
35
|
];
|
|
35
36
|
|
|
36
37
|
const PDF_SIGNATURE = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
37
|
-
const CFB_SIGNATURE = new Uint8Array([
|
|
38
|
-
0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
|
|
39
|
-
]);
|
|
40
|
-
const ENCRYPTION_INFO = utf16le("EncryptionInfo");
|
|
41
|
-
const ENCRYPTED_PACKAGE = utf16le("EncryptedPackage");
|
|
42
38
|
const PDF_TRAILER_SCAN_BYTES = 2048;
|
|
43
39
|
const MAX_MESSAGE_LENGTH = 200;
|
|
44
40
|
const PASSWORD_ERROR_REGEX = /password(?:-protected)?|no password given/i;
|
|
@@ -51,45 +47,6 @@ function toBuffer(bytes: Uint8Array): Buffer {
|
|
|
51
47
|
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
function utf16le(value: string): Uint8Array {
|
|
55
|
-
return new Uint8Array(Buffer.from(value, "utf16le"));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function hasPrefix(bytes: Uint8Array, prefix: Uint8Array): boolean {
|
|
59
|
-
if (bytes.length < prefix.length) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (let index = 0; index < prefix.length; index += 1) {
|
|
64
|
-
if (bytes[index] !== prefix[index]) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function includesBytes(bytes: Uint8Array, needle: Uint8Array): boolean {
|
|
73
|
-
if (needle.length === 0 || bytes.length < needle.length) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
outer: for (
|
|
78
|
-
let index = 0;
|
|
79
|
-
index <= bytes.length - needle.length;
|
|
80
|
-
index += 1
|
|
81
|
-
) {
|
|
82
|
-
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
83
|
-
if (bytes[index + offset] !== needle[offset]) {
|
|
84
|
-
continue outer;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
50
|
function isPasswordProtectedPdf(bytes: Uint8Array): boolean {
|
|
94
51
|
if (!hasPrefix(bytes, PDF_SIGNATURE)) {
|
|
95
52
|
return false;
|
|
@@ -113,14 +70,6 @@ function hasCompletePdfTrailer(bytes: Uint8Array): boolean {
|
|
|
113
70
|
return startXrefIndex >= 0 && eofIndex > startXrefIndex;
|
|
114
71
|
}
|
|
115
72
|
|
|
116
|
-
function isPasswordProtectedXlsx(bytes: Uint8Array): boolean {
|
|
117
|
-
return (
|
|
118
|
-
hasPrefix(bytes, CFB_SIGNATURE) &&
|
|
119
|
-
includesBytes(bytes, ENCRYPTION_INFO) &&
|
|
120
|
-
includesBytes(bytes, ENCRYPTED_PACKAGE)
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
73
|
function isPasswordProtected(input: ConvertInput): boolean {
|
|
125
74
|
if (input.ext === ".pdf") {
|
|
126
75
|
return isPasswordProtectedPdf(input.bytes);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte checks shared by document adapters: signature prefixes and the
|
|
3
|
+
* encrypted-OOXML (CFB container) test. Dependency-free so an adapter can
|
|
4
|
+
* use them without loading another adapter's parser stack.
|
|
5
|
+
*
|
|
6
|
+
* @module src/converters/adapters/shared/ooxml-protection
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const CFB_SIGNATURE = new Uint8Array([
|
|
10
|
+
0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export function utf16le(value: string): Uint8Array {
|
|
14
|
+
return new Uint8Array(Buffer.from(value, "utf16le"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ENCRYPTION_INFO = utf16le("EncryptionInfo");
|
|
18
|
+
const ENCRYPTED_PACKAGE = utf16le("EncryptedPackage");
|
|
19
|
+
|
|
20
|
+
export function hasPrefix(bytes: Uint8Array, prefix: Uint8Array): boolean {
|
|
21
|
+
if (bytes.length < prefix.length) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
25
|
+
if (bytes[index] !== prefix[index]) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function includesBytes(bytes: Uint8Array, needle: Uint8Array): boolean {
|
|
33
|
+
if (needle.length === 0 || bytes.length < needle.length) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
outer: for (
|
|
37
|
+
let index = 0;
|
|
38
|
+
index <= bytes.length - needle.length;
|
|
39
|
+
index += 1
|
|
40
|
+
) {
|
|
41
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
42
|
+
if (bytes[index + offset] !== needle[offset]) {
|
|
43
|
+
continue outer;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An encrypted (password-protected) OOXML workbook: a CFB container. */
|
|
52
|
+
export function isPasswordProtectedXlsx(bytes: Uint8Array): boolean {
|
|
53
|
+
return (
|
|
54
|
+
hasPrefix(bytes, CFB_SIGNATURE) &&
|
|
55
|
+
includesBytes(bytes, ENCRYPTION_INFO) &&
|
|
56
|
+
includesBytes(bytes, ENCRYPTED_PACKAGE)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel (.xlsx) converter: SheetJS workbook -> Markdown tables, linear in
|
|
3
|
+
* cells (fn-198).
|
|
4
|
+
*
|
|
5
|
+
* Produces the Markdown markitdown-ts produced for workbooks (one `## sheet`
|
|
6
|
+
* section and GFM table per sheet with a range) but renders each table
|
|
7
|
+
* directly from the sheet data instead of through HTML, jsdom and turndown,
|
|
8
|
+
* whose cost grew with columns x rows^2 and about 20 KB per cell. Pivot
|
|
9
|
+
* cache parts are not user content and SheetJS does not read them, as
|
|
10
|
+
* before.
|
|
11
|
+
*
|
|
12
|
+
* @module src/converters/adapters/xlsx/adapter
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as XLSX from "xlsx";
|
|
16
|
+
|
|
17
|
+
import type { Converter, ConvertInput, ConvertResult } from "../../types";
|
|
18
|
+
|
|
19
|
+
import { corruptError, permissionError, tooLargeError } from "../../errors";
|
|
20
|
+
import { ADAPTER_VERSIONS } from "../../versions";
|
|
21
|
+
import { isPasswordProtectedXlsx } from "../shared/ooxml-protection";
|
|
22
|
+
import { sheetToMarkdown } from "./sheet-markdown";
|
|
23
|
+
|
|
24
|
+
const CONVERTER_ID = "adapter/xlsx" as const;
|
|
25
|
+
const CONVERTER_VERSION = ADAPTER_VERSIONS.xlsx;
|
|
26
|
+
const XLSX_MIME =
|
|
27
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
28
|
+
const PASSWORD_ERROR_REGEX = /password(?:-protected)?|no password given/i;
|
|
29
|
+
/** Output under this length from a larger file is flagged as lossy. */
|
|
30
|
+
const SHORT_OUTPUT_CHARS = 10;
|
|
31
|
+
const SHORT_OUTPUT_MIN_BYTES = 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Markdown for a parsed workbook: one section per sheet with a range,
|
|
35
|
+
* normalized as markitdown-ts normalizes every result (line endings, outer
|
|
36
|
+
* whitespace, at most one blank line).
|
|
37
|
+
*/
|
|
38
|
+
export function workbookToMarkdown(workbook: XLSX.WorkBook): string {
|
|
39
|
+
let markdown = "";
|
|
40
|
+
for (const name of workbook.SheetNames) {
|
|
41
|
+
const sheet = workbook.Sheets[name];
|
|
42
|
+
if (sheet?.["!ref"]) {
|
|
43
|
+
markdown += `## ${name}\n${sheetToMarkdown(sheet)}\n\n`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return markdown
|
|
47
|
+
.replace(/\r\n|\r|\n/g, "\n")
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const xlsxAdapter: Converter = {
|
|
53
|
+
id: CONVERTER_ID,
|
|
54
|
+
version: CONVERTER_VERSION,
|
|
55
|
+
|
|
56
|
+
canHandle(mime: string, ext: string): boolean {
|
|
57
|
+
return ext === ".xlsx" || mime === XLSX_MIME;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
convert(input: ConvertInput): Promise<ConvertResult> {
|
|
61
|
+
return Promise.resolve(convertWorkbook(input));
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function convertWorkbook(input: ConvertInput): ConvertResult {
|
|
66
|
+
if (input.bytes.length > input.limits.maxBytes) {
|
|
67
|
+
return { ok: false, error: tooLargeError(input, CONVERTER_ID) };
|
|
68
|
+
}
|
|
69
|
+
if (isPasswordProtectedXlsx(input.bytes)) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: permissionError(
|
|
73
|
+
input,
|
|
74
|
+
CONVERTER_ID,
|
|
75
|
+
"File is password-protected",
|
|
76
|
+
undefined,
|
|
77
|
+
{ protection: "xlsx" }
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let workbook: XLSX.WorkBook;
|
|
83
|
+
try {
|
|
84
|
+
// Dense sheets store rows as arrays: smaller than one object per cell.
|
|
85
|
+
workbook = XLSX.read(input.bytes, { type: "buffer", dense: true });
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: PASSWORD_ERROR_REGEX.test(message)
|
|
91
|
+
? permissionError(
|
|
92
|
+
input,
|
|
93
|
+
CONVERTER_ID,
|
|
94
|
+
"File is password-protected",
|
|
95
|
+
cause,
|
|
96
|
+
{ protection: "xlsx" }
|
|
97
|
+
)
|
|
98
|
+
: corruptError(
|
|
99
|
+
input,
|
|
100
|
+
CONVERTER_ID,
|
|
101
|
+
"Could not read the .xlsx workbook",
|
|
102
|
+
cause
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const markdown = workbookToMarkdown(workbook);
|
|
108
|
+
if (!markdown) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: corruptError(input, CONVERTER_ID, "Empty conversion result"),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const lossy =
|
|
115
|
+
markdown.length < SHORT_OUTPUT_CHARS &&
|
|
116
|
+
input.bytes.length > SHORT_OUTPUT_MIN_BYTES;
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
value: {
|
|
120
|
+
markdown,
|
|
121
|
+
title: workbook.Props?.Title || "Untitled",
|
|
122
|
+
meta: {
|
|
123
|
+
converterId: CONVERTER_ID,
|
|
124
|
+
converterVersion: CONVERTER_VERSION,
|
|
125
|
+
sourceMime: input.mime,
|
|
126
|
+
warnings: lossy
|
|
127
|
+
? [{ code: "LOSSY", message: "Suspiciously short output" }]
|
|
128
|
+
: undefined,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|