@bendyline/squisq-formats 1.4.0 → 1.4.1
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 +180 -43
- package/dist/{chunk-CRTC6DEX.js → chunk-46FMDOWZ.js} +2 -2
- package/dist/{chunk-ON232HJR.js → chunk-5LSSWZPU.js} +2 -2
- package/dist/{chunk-APSIAEXL.js → chunk-D7IFPWNZ.js} +87 -43
- package/dist/chunk-D7IFPWNZ.js.map +1 -0
- package/dist/chunk-DQAZR57U.js +33 -0
- package/dist/chunk-DQAZR57U.js.map +1 -0
- package/dist/chunk-EHLGMSTE.js +273 -0
- package/dist/chunk-EHLGMSTE.js.map +1 -0
- package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
- package/dist/chunk-MJGRI6XR.js.map +1 -0
- package/dist/{chunk-JBNVE2GV.js → chunk-RFAPOKHJ.js} +9 -2
- package/dist/chunk-RFAPOKHJ.js.map +1 -0
- package/dist/chunk-RTS5XBZ7.js +526 -0
- package/dist/chunk-RTS5XBZ7.js.map +1 -0
- package/dist/{chunk-FMOIGA5T.js → chunk-SHLPSADL.js} +178 -5
- package/dist/chunk-SHLPSADL.js.map +1 -0
- package/dist/chunk-SSUPBUF5.js +15 -0
- package/dist/chunk-SSUPBUF5.js.map +1 -0
- package/dist/{chunk-BJQIJ4S4.js → chunk-WWBIKRNP.js} +17 -41
- package/dist/chunk-WWBIKRNP.js.map +1 -0
- package/dist/{chunk-I2GFTEOV.js → chunk-YESS7VY3.js} +77 -15
- package/dist/{chunk-I2GFTEOV.js.map → chunk-YESS7VY3.js.map} +1 -1
- package/dist/csv/index.d.ts +14 -1
- package/dist/csv/index.js +1 -1
- package/dist/docx/index.js +4 -3
- package/dist/epub/index.js +3 -2
- package/dist/html/index.d.ts +3 -0
- package/dist/html/index.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +26 -11
- package/dist/ooxml/index.d.ts +5 -1
- package/dist/ooxml/index.js +16 -12
- package/dist/pdf/index.js +2 -1
- package/dist/pptx/index.d.ts +18 -3
- package/dist/pptx/index.js +7 -4
- package/dist/registry/index.d.ts +167 -0
- package/dist/registry/index.js +17 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/xlsx/index.d.ts +44 -14
- package/dist/xlsx/index.js +4 -2
- package/package.json +7 -2
- package/src/__tests__/convert.test.ts +186 -0
- package/src/__tests__/csvImport.test.ts +34 -1
- package/src/__tests__/formatRegistry.test.ts +174 -0
- package/src/__tests__/lossyWarnings.test.ts +105 -0
- package/src/__tests__/pdfImport.test.ts +95 -1
- package/src/__tests__/pptxImport.test.ts +46 -1
- package/src/__tests__/roundTripAssets.test.ts +50 -0
- package/src/__tests__/roundTripMatrix.test.ts +10 -4
- package/src/__tests__/sharedContainer.test.ts +41 -0
- package/src/__tests__/sharedImages.test.ts +61 -0
- package/src/__tests__/xlsxExport.test.ts +164 -0
- package/src/csv/index.ts +25 -2
- package/src/docx/import.ts +6 -37
- package/src/html/imageUtils.ts +5 -19
- package/src/index.ts +29 -5
- package/src/ooxml/index.ts +3 -0
- package/src/ooxml/namespaces.ts +10 -0
- package/src/pdf/import.ts +145 -50
- package/src/pptx/import.ts +127 -10
- package/src/pptx/index.ts +1 -1
- package/src/registry/convert.ts +318 -0
- package/src/registry/defaultFormats.ts +323 -0
- package/src/registry/errors.ts +46 -0
- package/src/registry/index.ts +38 -0
- package/src/registry/registry.ts +48 -0
- package/src/registry/types.ts +107 -0
- package/src/shared/container.ts +28 -0
- package/src/shared/images.ts +44 -0
- package/src/xlsx/export.ts +253 -0
- package/src/xlsx/index.ts +10 -36
- package/dist/chunk-A3FHLTY5.js.map +0 -1
- package/dist/chunk-APSIAEXL.js.map +0 -1
- package/dist/chunk-BJQIJ4S4.js.map +0 -1
- package/dist/chunk-FMOIGA5T.js.map +0 -1
- package/dist/chunk-JBNVE2GV.js.map +0 -1
- package/dist/chunk-L53YIGYS.js +0 -141
- package/dist/chunk-L53YIGYS.js.map +0 -1
- package/dist/chunk-QSE7EWE7.js +0 -181
- package/dist/chunk-QSE7EWE7.js.map +0 -1
- /package/dist/{chunk-CRTC6DEX.js.map → chunk-46FMDOWZ.js.map} +0 -0
- /package/dist/{chunk-ON232HJR.js.map → chunk-5LSSWZPU.js.map} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the consolidated ext→MIME map (`shared/images.ts`) and the html
|
|
3
|
+
* `inferMimeType` wrapper that now delegates to it. This map is the union of
|
|
4
|
+
* the three previously-drifting copies (docx/pptx importers + html export).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { extToMime } from '../shared/images';
|
|
9
|
+
import { inferMimeType } from '../html/imageUtils';
|
|
10
|
+
|
|
11
|
+
describe('extToMime', () => {
|
|
12
|
+
it('is dot-tolerant and case-insensitive', () => {
|
|
13
|
+
expect(extToMime('png')).toBe('image/png');
|
|
14
|
+
expect(extToMime('.png')).toBe('image/png');
|
|
15
|
+
expect(extToMime('.PNG')).toBe('image/png');
|
|
16
|
+
expect(extToMime('PNG')).toBe('image/png');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('resolves .avif to image/avif (the correct answer where the old maps disagreed)', () => {
|
|
20
|
+
// html's map had avif→image/avif; the docx/pptx maps omitted it entirely
|
|
21
|
+
// (would have fallen back to octet-stream). The union picks image/avif.
|
|
22
|
+
expect(extToMime('avif')).toBe('image/avif');
|
|
23
|
+
expect(extToMime('.avif')).toBe('image/avif');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('covers the union of all three previous maps', () => {
|
|
27
|
+
// Raster/vector (all three had most of these)
|
|
28
|
+
expect(extToMime('jpg')).toBe('image/jpeg');
|
|
29
|
+
expect(extToMime('jpeg')).toBe('image/jpeg');
|
|
30
|
+
expect(extToMime('gif')).toBe('image/gif');
|
|
31
|
+
expect(extToMime('webp')).toBe('image/webp');
|
|
32
|
+
expect(extToMime('bmp')).toBe('image/bmp');
|
|
33
|
+
expect(extToMime('svg')).toBe('image/svg+xml');
|
|
34
|
+
// Only docx/pptx had these
|
|
35
|
+
expect(extToMime('tiff')).toBe('image/tiff');
|
|
36
|
+
expect(extToMime('tif')).toBe('image/tiff');
|
|
37
|
+
expect(extToMime('emf')).toBe('image/emf');
|
|
38
|
+
expect(extToMime('wmf')).toBe('image/wmf');
|
|
39
|
+
// Only html had these
|
|
40
|
+
expect(extToMime('ico')).toBe('image/x-icon');
|
|
41
|
+
expect(extToMime('mp3')).toBe('audio/mpeg');
|
|
42
|
+
expect(extToMime('wav')).toBe('audio/wav');
|
|
43
|
+
expect(extToMime('ogg')).toBe('audio/ogg');
|
|
44
|
+
expect(extToMime('mp4')).toBe('video/mp4');
|
|
45
|
+
expect(extToMime('webm')).toBe('video/webm');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns application/octet-stream for unknown / empty extensions', () => {
|
|
49
|
+
expect(extToMime('xyz')).toBe('application/octet-stream');
|
|
50
|
+
expect(extToMime('')).toBe('application/octet-stream');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('inferMimeType (html wrapper)', () => {
|
|
55
|
+
it('delegates to extToMime, keying off the filename extension', () => {
|
|
56
|
+
expect(inferMimeType('images/hero.PNG')).toBe('image/png');
|
|
57
|
+
expect(inferMimeType('photo.avif')).toBe('image/avif');
|
|
58
|
+
expect(inferMimeType('clip.webm')).toBe('video/webm');
|
|
59
|
+
expect(inferMimeType('noextension')).toBe('application/octet-stream');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import JSZip from 'jszip';
|
|
3
|
+
import { parseMarkdown, type MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
4
|
+
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
5
|
+
|
|
6
|
+
import { markdownDocToXlsx, docToXlsx } from '../xlsx/export';
|
|
7
|
+
import { xlsxToMarkdownDoc } from '../xlsx/import';
|
|
8
|
+
|
|
9
|
+
/** Read the workbook.xml + all worksheet XML out of an exported .xlsx buffer. */
|
|
10
|
+
async function unzip(buffer: ArrayBuffer): Promise<{
|
|
11
|
+
workbook: string;
|
|
12
|
+
sheets: string[];
|
|
13
|
+
paths: string[];
|
|
14
|
+
}> {
|
|
15
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
16
|
+
const paths = Object.keys(zip.files).sort();
|
|
17
|
+
const workbook = (await zip.file('xl/workbook.xml')?.async('string')) ?? '';
|
|
18
|
+
const sheetPaths = paths.filter((p) => /^xl\/worksheets\/sheet\d+\.xml$/.test(p));
|
|
19
|
+
const sheets = await Promise.all(sheetPaths.map((p) => zip.file(p)!.async('string')));
|
|
20
|
+
return { workbook, sheets, paths };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Extract sheet names (in order) from a workbook.xml string. */
|
|
24
|
+
function sheetNames(workbook: string): string[] {
|
|
25
|
+
return [...workbook.matchAll(/<sheet\s+name="([^"]*)"/g)].map((m) => m[1]!);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Build a single-heading + single-table document without touching the parser. */
|
|
29
|
+
function docWithHeading(headingText: string): MarkdownDocument {
|
|
30
|
+
return {
|
|
31
|
+
type: 'document',
|
|
32
|
+
children: [
|
|
33
|
+
{ type: 'heading', depth: 1, children: [{ type: 'text', value: headingText }] },
|
|
34
|
+
{
|
|
35
|
+
type: 'table',
|
|
36
|
+
children: [
|
|
37
|
+
{
|
|
38
|
+
type: 'tableRow',
|
|
39
|
+
children: [
|
|
40
|
+
{ type: 'tableCell', isHeader: true, children: [{ type: 'text', value: 'A' }] },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'tableRow',
|
|
45
|
+
children: [{ type: 'tableCell', children: [{ type: 'text', value: 'x' }] }],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('markdownDocToXlsx', () => {
|
|
54
|
+
it('produces a structurally valid package (content types + rels + workbook)', async () => {
|
|
55
|
+
const md = parseMarkdown('# Data\n\n| A | B |\n| - | - |\n| 1 | 2 |\n');
|
|
56
|
+
const buffer = await markdownDocToXlsx(md);
|
|
57
|
+
const { paths } = await unzip(buffer);
|
|
58
|
+
|
|
59
|
+
expect(paths).toContain('[Content_Types].xml');
|
|
60
|
+
expect(paths).toContain('_rels/.rels');
|
|
61
|
+
expect(paths).toContain('xl/workbook.xml');
|
|
62
|
+
expect(paths).toContain('xl/_rels/workbook.xml.rels');
|
|
63
|
+
expect(paths).toContain('xl/worksheets/sheet1.xml');
|
|
64
|
+
expect(paths).toContain('xl/styles.xml');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('names each sheet from the nearest preceding heading', async () => {
|
|
68
|
+
const md = parseMarkdown('# Sales\n\n| A |\n| - |\n| x |\n');
|
|
69
|
+
const { workbook } = await unzip(await markdownDocToXlsx(md));
|
|
70
|
+
expect(sheetNames(workbook)).toEqual(['Sales']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('sanitizes invalid Excel characters and caps names at 31 chars', async () => {
|
|
74
|
+
const { workbook } = await unzip(await markdownDocToXlsx(docWithHeading('a:b/c\\d[e]f?g*h')));
|
|
75
|
+
expect(sheetNames(workbook)).toEqual(['abcdefgh']);
|
|
76
|
+
|
|
77
|
+
const { workbook: wb2 } = await unzip(await markdownDocToXlsx(docWithHeading('X'.repeat(50))));
|
|
78
|
+
expect(sheetNames(wb2)[0]!.length).toBe(31);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('de-duplicates repeated sheet names (Name, Name2, Name3, …)', async () => {
|
|
82
|
+
const md = parseMarkdown(
|
|
83
|
+
'# Report\n\n| A |\n| - |\n| 1 |\n\n# Report\n\n| B |\n| - |\n| 2 |\n\n# Report\n\n| C |\n| - |\n| 3 |\n',
|
|
84
|
+
);
|
|
85
|
+
const { workbook } = await unzip(await markdownDocToXlsx(md));
|
|
86
|
+
expect(sheetNames(workbook)).toEqual(['Report', 'Report2', 'Report3']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('falls back to Sheet1, Sheet2 when a table has no preceding heading', async () => {
|
|
90
|
+
const md = parseMarkdown('| A |\n| - |\n| 1 |\n\n| B |\n| - |\n| 2 |\n');
|
|
91
|
+
const { workbook } = await unzip(await markdownDocToXlsx(md));
|
|
92
|
+
expect(sheetNames(workbook)).toEqual(['Sheet1', 'Sheet2']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('emits plain numbers as numeric cells and everything else as inline strings', async () => {
|
|
96
|
+
const md = parseMarkdown(
|
|
97
|
+
'# T\n\n| Label | N |\n| - | - |\n| Alpha | 42 |\n| Neg | -3.5 |\n| Mixed | 120 req/s |\n',
|
|
98
|
+
);
|
|
99
|
+
const { sheets } = await unzip(await markdownDocToXlsx(md));
|
|
100
|
+
const xml = sheets[0]!;
|
|
101
|
+
|
|
102
|
+
// Numeric cells: <v>…</v> without t="inlineStr"
|
|
103
|
+
expect(xml).toContain('<v>42</v>');
|
|
104
|
+
expect(xml).toContain('<v>-3.5</v>');
|
|
105
|
+
// String cells: inlineStr
|
|
106
|
+
expect(xml).toContain('t="inlineStr"');
|
|
107
|
+
expect(xml).toContain('>Alpha<');
|
|
108
|
+
expect(xml).toContain('>120 req/s<');
|
|
109
|
+
// "120 req/s" must NOT be numeric
|
|
110
|
+
expect(xml).not.toContain('<v>120 req/s</v>');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('maps multiple tables to multiple worksheets', async () => {
|
|
114
|
+
const md = parseMarkdown('# First\n\n| A |\n| - |\n| 1 |\n\n# Second\n\n| B |\n| - |\n| 2 |\n');
|
|
115
|
+
const { sheets, workbook } = await unzip(await markdownDocToXlsx(md));
|
|
116
|
+
expect(sheets.length).toBe(2);
|
|
117
|
+
expect(sheetNames(workbook)).toEqual(['First', 'Second']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('produces a valid empty file (single sheet) when there are no tables', async () => {
|
|
121
|
+
const md = parseMarkdown('# Just prose\n\nNo tables here at all.\n');
|
|
122
|
+
const buffer = await markdownDocToXlsx(md);
|
|
123
|
+
const { sheets, workbook } = await unzip(buffer);
|
|
124
|
+
expect(sheets.length).toBe(1);
|
|
125
|
+
expect(sheetNames(workbook)).toEqual(['Sheet1']);
|
|
126
|
+
|
|
127
|
+
// Still round-trips without throwing.
|
|
128
|
+
const roundTrip = await xlsxToMarkdownDoc(buffer);
|
|
129
|
+
expect(roundTrip.type).toBe('document');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('honors a custom sheetNamePrefix for auto-named sheets', async () => {
|
|
133
|
+
const md = parseMarkdown('| A |\n| - |\n| 1 |\n');
|
|
134
|
+
const { workbook } = await unzip(await markdownDocToXlsx(md, { sheetNamePrefix: 'Tab' }));
|
|
135
|
+
expect(sheetNames(workbook)).toEqual(['Tab1']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('survives a full round-trip (cell text preserved through import)', async () => {
|
|
139
|
+
const md = parseMarkdown(
|
|
140
|
+
'# Metrics\n\n| Metric | Value |\n| - | - |\n| Throughput | 120 |\n| Errors | 0.2% |\n',
|
|
141
|
+
);
|
|
142
|
+
const roundTrip = await xlsxToMarkdownDoc(await markdownDocToXlsx(md));
|
|
143
|
+
const table = roundTrip.children.find((n) => n.type === 'table');
|
|
144
|
+
expect(table).toBeDefined();
|
|
145
|
+
|
|
146
|
+
const flat = JSON.stringify(roundTrip);
|
|
147
|
+
for (const cell of ['Metric', 'Value', 'Throughput', '120', 'Errors', '0.2%']) {
|
|
148
|
+
expect(flat).toContain(cell);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('docToXlsx converts a Doc via the markdown table model', async () => {
|
|
153
|
+
const doc = markdownToDoc(parseMarkdown('# Report\n\n| K | V |\n| - | - |\n| Users | 99 |\n'));
|
|
154
|
+
const buffer = await docToXlsx(doc);
|
|
155
|
+
const { sheets, workbook } = await unzip(buffer);
|
|
156
|
+
expect(sheets.length).toBeGreaterThanOrEqual(1);
|
|
157
|
+
expect(sheetNames(workbook).length).toBeGreaterThanOrEqual(1);
|
|
158
|
+
|
|
159
|
+
const roundTrip = await xlsxToMarkdownDoc(buffer);
|
|
160
|
+
const flat = JSON.stringify(roundTrip);
|
|
161
|
+
expect(flat).toContain('Users');
|
|
162
|
+
expect(flat).toContain('99');
|
|
163
|
+
});
|
|
164
|
+
});
|
package/src/csv/index.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface CsvImportOptions {
|
|
|
31
31
|
export interface CsvExportOptions {
|
|
32
32
|
/** Field delimiter. Default `,`. */
|
|
33
33
|
delimiter?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Zero-based index of the table to export when the document contains more
|
|
36
|
+
* than one. Default 0 (the first table). An explicitly provided index that
|
|
37
|
+
* doesn't match a table in the document is an error.
|
|
38
|
+
*/
|
|
39
|
+
tableIndex?: number;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
async function toText(data: ArrayBuffer | Blob | string): Promise<string> {
|
|
@@ -152,10 +158,27 @@ function cellText(cell: MarkdownTableCell): string {
|
|
|
152
158
|
return walk(cell.children);
|
|
153
159
|
}
|
|
154
160
|
|
|
155
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Serialize one table in a MarkdownDocument to CSV text.
|
|
163
|
+
*
|
|
164
|
+
* By default the first table is exported. Documents with multiple tables can
|
|
165
|
+
* select another via `options.tableIndex` (zero-based). An explicit
|
|
166
|
+
* `tableIndex` that is out of range throws; the implicit first-table default
|
|
167
|
+
* on a table-less document returns an empty string (back-compat).
|
|
168
|
+
*/
|
|
156
169
|
export function markdownDocToCsv(doc: MarkdownDocument, options: CsvExportOptions = {}): string {
|
|
157
170
|
const delimiter = options.delimiter ?? ',';
|
|
158
|
-
const
|
|
171
|
+
const tables = doc.children.filter((n): n is MarkdownTable => n.type === 'table');
|
|
172
|
+
const index = options.tableIndex ?? 0;
|
|
173
|
+
if (
|
|
174
|
+
options.tableIndex !== undefined &&
|
|
175
|
+
(!Number.isInteger(index) || index < 0 || index >= tables.length)
|
|
176
|
+
) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`CSV export: tableIndex ${index} is out of range — the document contains ${tables.length} table(s).`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const table = tables[index];
|
|
159
182
|
if (!table) return '';
|
|
160
183
|
return table.children
|
|
161
184
|
.map((row) =>
|
package/src/docx/import.ts
CHANGED
|
@@ -50,8 +50,9 @@ import type {
|
|
|
50
50
|
import { openPackage, getPartXml, getPartBinary, getPartRelationships } from '../ooxml/reader.js';
|
|
51
51
|
import type { OoxmlPackage, Relationship } from '../ooxml/types.js';
|
|
52
52
|
import { NS_WML, NS_R } from '../ooxml/namespaces.js';
|
|
53
|
-
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
54
53
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
54
|
+
import { buildContainer } from '../shared/container.js';
|
|
55
|
+
import { extToMime } from '../shared/images.js';
|
|
55
56
|
import {
|
|
56
57
|
HEADING_STYLE_MAP,
|
|
57
58
|
QUOTE_STYLE_IDS,
|
|
@@ -141,33 +142,15 @@ export async function docxToContainer(
|
|
|
141
142
|
const ctx = await buildImportContext(pkg, { ...options, extractImages: true });
|
|
142
143
|
|
|
143
144
|
const documentXml = await getPartXml(pkg, 'word/document.xml');
|
|
144
|
-
if (!documentXml)
|
|
145
|
-
const container = new MemoryContentContainer();
|
|
146
|
-
await container.writeDocument('');
|
|
147
|
-
return container;
|
|
148
|
-
}
|
|
145
|
+
if (!documentXml) return buildContainer('', []);
|
|
149
146
|
|
|
150
147
|
const body = getFirstElement(documentXml, 'body');
|
|
151
|
-
if (!body)
|
|
152
|
-
const container = new MemoryContentContainer();
|
|
153
|
-
await container.writeDocument('');
|
|
154
|
-
return container;
|
|
155
|
-
}
|
|
148
|
+
if (!body) return buildContainer('', []);
|
|
156
149
|
|
|
157
150
|
const blocks = await convertBody(body, ctx);
|
|
158
151
|
const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
|
|
159
152
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Build container with markdown + images
|
|
163
|
-
const container = new MemoryContentContainer();
|
|
164
|
-
await container.writeDocument(markdown);
|
|
165
|
-
|
|
166
|
-
for (const [path, { data: imageData, mimeType }] of ctx.extractedImages) {
|
|
167
|
-
await container.writeFile(path, new Uint8Array(imageData), mimeType);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return container;
|
|
153
|
+
return buildContainer(stringifyMarkdown(markdownDoc), ctx.extractedImages);
|
|
171
154
|
}
|
|
172
155
|
|
|
173
156
|
// ============================================
|
|
@@ -630,7 +613,7 @@ async function extractImage(el: Element, ctx: ImportContext): Promise<MarkdownIm
|
|
|
630
613
|
// Determine extension and MIME type
|
|
631
614
|
const dot = target.lastIndexOf('.');
|
|
632
615
|
const ext = dot !== -1 ? target.slice(dot).toLowerCase() : '.png';
|
|
633
|
-
const mimeType =
|
|
616
|
+
const mimeType = extToMime(ext);
|
|
634
617
|
|
|
635
618
|
// Generate a unique image path
|
|
636
619
|
ctx.imageCounter++;
|
|
@@ -650,20 +633,6 @@ async function extractImage(el: Element, ctx: ImportContext): Promise<MarkdownIm
|
|
|
650
633
|
};
|
|
651
634
|
}
|
|
652
635
|
|
|
653
|
-
const IMAGE_MIME_MAP: Record<string, string> = {
|
|
654
|
-
'.png': 'image/png',
|
|
655
|
-
'.jpg': 'image/jpeg',
|
|
656
|
-
'.jpeg': 'image/jpeg',
|
|
657
|
-
'.gif': 'image/gif',
|
|
658
|
-
'.bmp': 'image/bmp',
|
|
659
|
-
'.tiff': 'image/tiff',
|
|
660
|
-
'.tif': 'image/tiff',
|
|
661
|
-
'.svg': 'image/svg+xml',
|
|
662
|
-
'.webp': 'image/webp',
|
|
663
|
-
'.emf': 'image/emf',
|
|
664
|
-
'.wmf': 'image/wmf',
|
|
665
|
-
};
|
|
666
|
-
|
|
667
636
|
/** Recursively find the first descendant element with the given local name. */
|
|
668
637
|
function findDescendant(el: Element, localName: string): Element | null {
|
|
669
638
|
for (const child of Array.from(el.children)) {
|
package/src/html/imageUtils.ts
CHANGED
|
@@ -5,31 +5,17 @@
|
|
|
5
5
|
* and inferring MIME types from filenames.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const MIME_MAP: Record<string, string> = {
|
|
10
|
-
jpg: 'image/jpeg',
|
|
11
|
-
jpeg: 'image/jpeg',
|
|
12
|
-
png: 'image/png',
|
|
13
|
-
gif: 'image/gif',
|
|
14
|
-
webp: 'image/webp',
|
|
15
|
-
svg: 'image/svg+xml',
|
|
16
|
-
ico: 'image/x-icon',
|
|
17
|
-
bmp: 'image/bmp',
|
|
18
|
-
avif: 'image/avif',
|
|
19
|
-
mp3: 'audio/mpeg',
|
|
20
|
-
wav: 'audio/wav',
|
|
21
|
-
ogg: 'audio/ogg',
|
|
22
|
-
mp4: 'video/mp4',
|
|
23
|
-
webm: 'video/webm',
|
|
24
|
-
};
|
|
8
|
+
import { extToMime } from '../shared/images.js';
|
|
25
9
|
|
|
26
10
|
/**
|
|
27
11
|
* Infer a MIME type from a filename's extension.
|
|
28
12
|
* Returns 'application/octet-stream' for unknown types.
|
|
13
|
+
*
|
|
14
|
+
* Thin wrapper over the shared {@link extToMime} map so html export, docx
|
|
15
|
+
* import, and pptx import all agree on extension → MIME.
|
|
29
16
|
*/
|
|
30
17
|
export function inferMimeType(filename: string): string {
|
|
31
|
-
|
|
32
|
-
return MIME_MAP[ext] ?? 'application/octet-stream';
|
|
18
|
+
return extToMime(filename.split('.').pop() ?? '');
|
|
33
19
|
}
|
|
34
20
|
|
|
35
21
|
/**
|
package/src/index.ts
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* Supported formats:
|
|
8
8
|
* - **DOCX** — Microsoft Word (import + export) ✅
|
|
9
9
|
* - **PDF** — Portable Document Format (import + export) ✅
|
|
10
|
-
* - **PPTX** — Microsoft PowerPoint (export
|
|
11
|
-
* - **XLSX** — Microsoft Excel (import ✅, export
|
|
10
|
+
* - **PPTX** — Microsoft PowerPoint (import + export ✅; import extracts slide-level embedded images)
|
|
11
|
+
* - **XLSX** — Microsoft Excel (import ✅, export ✅ tables-only)
|
|
12
12
|
* - **CSV** — Comma-separated values (import ✅, export ✅)
|
|
13
|
+
* - **HTML** — import + export (single-file, ZIP, plain, and player-embedding)
|
|
14
|
+
* - **EPUB** — export ✅
|
|
13
15
|
*
|
|
14
16
|
* All converters run in the browser — no server or native binaries required.
|
|
15
|
-
* The shared `ooxml/` subpath export provides reusable OOXML infrastructure
|
|
17
|
+
* The shared `ooxml/` subpath export provides reusable OOXML infrastructure, and the
|
|
18
|
+
* `registry` subpath exposes a format registry + `convert()` pipeline over all of the above.
|
|
16
19
|
*
|
|
17
20
|
* @example
|
|
18
21
|
* ```ts
|
|
@@ -29,11 +32,11 @@
|
|
|
29
32
|
export { markdownDocToDocx, docToDocx, docxToMarkdownDoc, docxToDoc } from './docx/index.js';
|
|
30
33
|
export type { DocxExportOptions, DocxImportOptions } from './docx/index.js';
|
|
31
34
|
|
|
32
|
-
// PPTX (
|
|
35
|
+
// PPTX (import + export)
|
|
33
36
|
export { markdownDocToPptx, docToPptx, pptxToMarkdownDoc, pptxToDoc } from './pptx/index.js';
|
|
34
37
|
export type { PptxExportOptions, PptxImportOptions } from './pptx/index.js';
|
|
35
38
|
|
|
36
|
-
// XLSX (import
|
|
39
|
+
// XLSX (import + export; export is tables-only → one worksheet per markdown table)
|
|
37
40
|
export { markdownDocToXlsx, docToXlsx, xlsxToMarkdownDoc, xlsxToDoc } from './xlsx/index.js';
|
|
38
41
|
export type { XlsxExportOptions, XlsxImportOptions } from './xlsx/index.js';
|
|
39
42
|
|
|
@@ -60,3 +63,24 @@ export type { HtmlImportOptions } from './html/index.js';
|
|
|
60
63
|
// EPUB (export)
|
|
61
64
|
export { markdownDocToEpub, docToEpub } from './epub/index.js';
|
|
62
65
|
export type { EpubExportOptions } from './epub/index.js';
|
|
66
|
+
|
|
67
|
+
// Format registry + programmatic convert()
|
|
68
|
+
export {
|
|
69
|
+
convert,
|
|
70
|
+
createRegistry,
|
|
71
|
+
defaultRegistry,
|
|
72
|
+
defaultFormats,
|
|
73
|
+
ConversionError,
|
|
74
|
+
BUILTIN_FORMAT_IDS,
|
|
75
|
+
} from './registry/index.js';
|
|
76
|
+
export type {
|
|
77
|
+
FormatId,
|
|
78
|
+
ConversionResult,
|
|
79
|
+
NormalizedInput,
|
|
80
|
+
ConvertOptions,
|
|
81
|
+
FormatDefinition,
|
|
82
|
+
FormatRegistry,
|
|
83
|
+
ConvertSource,
|
|
84
|
+
ConversionErrorCode,
|
|
85
|
+
ConversionErrorOptions,
|
|
86
|
+
} from './registry/index.js';
|
package/src/ooxml/index.ts
CHANGED
package/src/ooxml/namespaces.ts
CHANGED
|
@@ -184,3 +184,13 @@ export const REL_SLIDE_MASTER =
|
|
|
184
184
|
|
|
185
185
|
export const CONTENT_TYPE_XLSX_WORKBOOK =
|
|
186
186
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml';
|
|
187
|
+
|
|
188
|
+
export const CONTENT_TYPE_XLSX_WORKSHEET =
|
|
189
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
|
190
|
+
|
|
191
|
+
export const CONTENT_TYPE_XLSX_STYLES =
|
|
192
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml';
|
|
193
|
+
|
|
194
|
+
/** Relationship type: Worksheet */
|
|
195
|
+
export const REL_WORKSHEET =
|
|
196
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
|