@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.
Files changed (83) hide show
  1. package/README.md +180 -43
  2. package/dist/{chunk-CRTC6DEX.js → chunk-46FMDOWZ.js} +2 -2
  3. package/dist/{chunk-ON232HJR.js → chunk-5LSSWZPU.js} +2 -2
  4. package/dist/{chunk-APSIAEXL.js → chunk-D7IFPWNZ.js} +87 -43
  5. package/dist/chunk-D7IFPWNZ.js.map +1 -0
  6. package/dist/chunk-DQAZR57U.js +33 -0
  7. package/dist/chunk-DQAZR57U.js.map +1 -0
  8. package/dist/chunk-EHLGMSTE.js +273 -0
  9. package/dist/chunk-EHLGMSTE.js.map +1 -0
  10. package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
  11. package/dist/chunk-MJGRI6XR.js.map +1 -0
  12. package/dist/{chunk-JBNVE2GV.js → chunk-RFAPOKHJ.js} +9 -2
  13. package/dist/chunk-RFAPOKHJ.js.map +1 -0
  14. package/dist/chunk-RTS5XBZ7.js +526 -0
  15. package/dist/chunk-RTS5XBZ7.js.map +1 -0
  16. package/dist/{chunk-FMOIGA5T.js → chunk-SHLPSADL.js} +178 -5
  17. package/dist/chunk-SHLPSADL.js.map +1 -0
  18. package/dist/chunk-SSUPBUF5.js +15 -0
  19. package/dist/chunk-SSUPBUF5.js.map +1 -0
  20. package/dist/{chunk-BJQIJ4S4.js → chunk-WWBIKRNP.js} +17 -41
  21. package/dist/chunk-WWBIKRNP.js.map +1 -0
  22. package/dist/{chunk-I2GFTEOV.js → chunk-YESS7VY3.js} +77 -15
  23. package/dist/{chunk-I2GFTEOV.js.map → chunk-YESS7VY3.js.map} +1 -1
  24. package/dist/csv/index.d.ts +14 -1
  25. package/dist/csv/index.js +1 -1
  26. package/dist/docx/index.js +4 -3
  27. package/dist/epub/index.js +3 -2
  28. package/dist/html/index.d.ts +3 -0
  29. package/dist/html/index.js +3 -2
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +26 -11
  32. package/dist/ooxml/index.d.ts +5 -1
  33. package/dist/ooxml/index.js +16 -12
  34. package/dist/pdf/index.js +2 -1
  35. package/dist/pptx/index.d.ts +18 -3
  36. package/dist/pptx/index.js +7 -4
  37. package/dist/registry/index.d.ts +167 -0
  38. package/dist/registry/index.js +17 -0
  39. package/dist/registry/index.js.map +1 -0
  40. package/dist/xlsx/index.d.ts +44 -14
  41. package/dist/xlsx/index.js +4 -2
  42. package/package.json +7 -2
  43. package/src/__tests__/convert.test.ts +186 -0
  44. package/src/__tests__/csvImport.test.ts +34 -1
  45. package/src/__tests__/formatRegistry.test.ts +174 -0
  46. package/src/__tests__/lossyWarnings.test.ts +105 -0
  47. package/src/__tests__/pdfImport.test.ts +95 -1
  48. package/src/__tests__/pptxImport.test.ts +46 -1
  49. package/src/__tests__/roundTripAssets.test.ts +50 -0
  50. package/src/__tests__/roundTripMatrix.test.ts +10 -4
  51. package/src/__tests__/sharedContainer.test.ts +41 -0
  52. package/src/__tests__/sharedImages.test.ts +61 -0
  53. package/src/__tests__/xlsxExport.test.ts +164 -0
  54. package/src/csv/index.ts +25 -2
  55. package/src/docx/import.ts +6 -37
  56. package/src/html/imageUtils.ts +5 -19
  57. package/src/index.ts +29 -5
  58. package/src/ooxml/index.ts +3 -0
  59. package/src/ooxml/namespaces.ts +10 -0
  60. package/src/pdf/import.ts +145 -50
  61. package/src/pptx/import.ts +127 -10
  62. package/src/pptx/index.ts +1 -1
  63. package/src/registry/convert.ts +318 -0
  64. package/src/registry/defaultFormats.ts +323 -0
  65. package/src/registry/errors.ts +46 -0
  66. package/src/registry/index.ts +38 -0
  67. package/src/registry/registry.ts +48 -0
  68. package/src/registry/types.ts +107 -0
  69. package/src/shared/container.ts +28 -0
  70. package/src/shared/images.ts +44 -0
  71. package/src/xlsx/export.ts +253 -0
  72. package/src/xlsx/index.ts +10 -36
  73. package/dist/chunk-A3FHLTY5.js.map +0 -1
  74. package/dist/chunk-APSIAEXL.js.map +0 -1
  75. package/dist/chunk-BJQIJ4S4.js.map +0 -1
  76. package/dist/chunk-FMOIGA5T.js.map +0 -1
  77. package/dist/chunk-JBNVE2GV.js.map +0 -1
  78. package/dist/chunk-L53YIGYS.js +0 -141
  79. package/dist/chunk-L53YIGYS.js.map +0 -1
  80. package/dist/chunk-QSE7EWE7.js +0 -181
  81. package/dist/chunk-QSE7EWE7.js.map +0 -1
  82. /package/dist/{chunk-CRTC6DEX.js.map → chunk-46FMDOWZ.js.map} +0 -0
  83. /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
- /** Serialize the first table in a MarkdownDocument to CSV text. */
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 table = doc.children.find((n) => n.type === 'table') as MarkdownTable | undefined;
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) =>
@@ -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
- const markdown = stringifyMarkdown(markdownDoc);
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 = IMAGE_MIME_MAP[ext] ?? 'application/octet-stream';
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)) {
@@ -5,31 +5,17 @@
5
5
  * and inferring MIME types from filenames.
6
6
  */
7
7
 
8
- /** Map of file extensions to MIME types */
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
- const ext = filename.split('.').pop()?.toLowerCase() ?? '';
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 ✅, import )
11
- * - **XLSX** — Microsoft Excel (import ✅, export planned)
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 (export implemented, import stub)
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 implemented, export stub)
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';
@@ -91,4 +91,7 @@ export {
91
91
  REL_SLIDE_LAYOUT,
92
92
  REL_SLIDE_MASTER,
93
93
  CONTENT_TYPE_XLSX_WORKBOOK,
94
+ CONTENT_TYPE_XLSX_WORKSHEET,
95
+ CONTENT_TYPE_XLSX_STYLES,
96
+ REL_WORKSHEET,
94
97
  } from './namespaces.js';
@@ -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';