@bendyline/squisq-formats 1.3.1 → 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 (107) hide show
  1. package/README.md +180 -43
  2. package/dist/{chunk-33YRFXZZ.js → chunk-46FMDOWZ.js} +260 -14
  3. package/dist/chunk-46FMDOWZ.js.map +1 -0
  4. package/dist/{chunk-ERZ627GR.js → chunk-5LSSWZPU.js} +12 -27
  5. package/dist/chunk-5LSSWZPU.js.map +1 -0
  6. package/dist/chunk-6M7Z25LA.js +46 -0
  7. package/dist/chunk-6M7Z25LA.js.map +1 -0
  8. package/dist/{chunk-S3Y7H2BK.js → chunk-D7IFPWNZ.js} +95 -50
  9. package/dist/chunk-D7IFPWNZ.js.map +1 -0
  10. package/dist/chunk-DQAZR57U.js +33 -0
  11. package/dist/chunk-DQAZR57U.js.map +1 -0
  12. package/dist/chunk-EHLGMSTE.js +273 -0
  13. package/dist/chunk-EHLGMSTE.js.map +1 -0
  14. package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
  15. package/dist/chunk-MJGRI6XR.js.map +1 -0
  16. package/dist/chunk-PN52A5AA.js +32 -0
  17. package/dist/chunk-PN52A5AA.js.map +1 -0
  18. package/dist/chunk-RFAPOKHJ.js +123 -0
  19. package/dist/chunk-RFAPOKHJ.js.map +1 -0
  20. package/dist/chunk-RTS5XBZ7.js +526 -0
  21. package/dist/chunk-RTS5XBZ7.js.map +1 -0
  22. package/dist/{chunk-7DEUGIOO.js → chunk-SHLPSADL.js} +120 -2
  23. package/dist/chunk-SHLPSADL.js.map +1 -0
  24. package/dist/chunk-SSUPBUF5.js +15 -0
  25. package/dist/chunk-SSUPBUF5.js.map +1 -0
  26. package/dist/{chunk-UDS45KUJ.js → chunk-WWBIKRNP.js} +40 -80
  27. package/dist/chunk-WWBIKRNP.js.map +1 -0
  28. package/dist/{chunk-VN2KEOYB.js → chunk-YESS7VY3.js} +379 -87
  29. package/dist/chunk-YESS7VY3.js.map +1 -0
  30. package/dist/csv/index.d.ts +50 -0
  31. package/dist/csv/index.js +13 -0
  32. package/dist/csv/index.js.map +1 -0
  33. package/dist/docx/index.js +6 -3
  34. package/dist/epub/index.js +4 -2
  35. package/dist/html/index.d.ts +42 -2
  36. package/dist/html/index.js +9 -2
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +47 -14
  39. package/dist/ooxml/index.d.ts +5 -1
  40. package/dist/ooxml/index.js +13 -9
  41. package/dist/pdf/index.js +2 -1
  42. package/dist/pptx/index.d.ts +36 -19
  43. package/dist/pptx/index.js +9 -2
  44. package/dist/registry/index.d.ts +167 -0
  45. package/dist/registry/index.js +17 -0
  46. package/dist/registry/index.js.map +1 -0
  47. package/dist/xlsx/index.d.ts +60 -29
  48. package/dist/xlsx/index.js +4 -1
  49. package/package.json +12 -2
  50. package/src/__tests__/convert.test.ts +186 -0
  51. package/src/__tests__/csvImport.test.ts +84 -0
  52. package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
  53. package/src/__tests__/formatRegistry.test.ts +174 -0
  54. package/src/__tests__/htmlImport.test.ts +57 -0
  55. package/src/__tests__/lossyWarnings.test.ts +105 -0
  56. package/src/__tests__/pdfImport.test.ts +95 -1
  57. package/src/__tests__/plainHtml.test.ts +31 -0
  58. package/src/__tests__/pptxExport.test.ts +138 -0
  59. package/src/__tests__/pptxImport.test.ts +115 -0
  60. package/src/__tests__/roundTripAssets.test.ts +50 -0
  61. package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
  62. package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
  63. package/src/__tests__/roundTripMatrix.test.ts +142 -0
  64. package/src/__tests__/sharedContainer.test.ts +41 -0
  65. package/src/__tests__/sharedImages.test.ts +61 -0
  66. package/src/__tests__/xlsxExport.test.ts +164 -0
  67. package/src/__tests__/xlsxImport.test.ts +80 -0
  68. package/src/csv/index.ts +188 -0
  69. package/src/docx/export.ts +27 -44
  70. package/src/docx/import.ts +7 -39
  71. package/src/epub/export.ts +10 -27
  72. package/src/html/imageUtils.ts +5 -19
  73. package/src/html/import.ts +297 -0
  74. package/src/html/index.ts +4 -0
  75. package/src/html/plainHtml.ts +66 -16
  76. package/src/index.ts +36 -5
  77. package/src/ooxml/index.ts +3 -0
  78. package/src/ooxml/namespaces.ts +13 -0
  79. package/src/pdf/export.ts +10 -5
  80. package/src/pdf/import.ts +146 -51
  81. package/src/pptx/export.ts +225 -85
  82. package/src/pptx/import.ts +273 -0
  83. package/src/pptx/index.ts +10 -28
  84. package/src/registry/convert.ts +318 -0
  85. package/src/registry/defaultFormats.ts +323 -0
  86. package/src/registry/errors.ts +46 -0
  87. package/src/registry/index.ts +38 -0
  88. package/src/registry/registry.ts +48 -0
  89. package/src/registry/types.ts +107 -0
  90. package/src/shared/container.ts +28 -0
  91. package/src/shared/images.ts +44 -0
  92. package/src/shared/inlineRuns.ts +99 -0
  93. package/src/shared/text.ts +41 -0
  94. package/src/xlsx/export.ts +253 -0
  95. package/src/xlsx/import.ts +159 -0
  96. package/src/xlsx/index.ts +18 -61
  97. package/dist/chunk-33YRFXZZ.js.map +0 -1
  98. package/dist/chunk-67KIJHV2.js +0 -21
  99. package/dist/chunk-67KIJHV2.js.map +0 -1
  100. package/dist/chunk-7DEUGIOO.js.map +0 -1
  101. package/dist/chunk-A3FHLTY5.js.map +0 -1
  102. package/dist/chunk-ERZ627GR.js.map +0 -1
  103. package/dist/chunk-S3Y7H2BK.js.map +0 -1
  104. package/dist/chunk-UDS45KUJ.js.map +0 -1
  105. package/dist/chunk-VN2KEOYB.js.map +0 -1
  106. package/dist/chunk-YN5HFCEW.js +0 -120
  107. package/dist/chunk-YN5HFCEW.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { Doc } from '@bendyline/squisq/schemas';
2
2
  import { MarkdownDocument } from '@bendyline/squisq/markdown';
3
+ import { ContentContainer } from '@bendyline/squisq/storage';
3
4
 
4
5
  /**
5
6
  * PPTX Export
@@ -75,6 +76,37 @@ declare function markdownDocToPptx(doc: MarkdownDocument, options?: PptxExportOp
75
76
  */
76
77
  declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<ArrayBuffer>;
77
78
 
79
+ /**
80
+ * PPTX import — PresentationML (.pptx) → MarkdownDocument.
81
+ *
82
+ * Reuses the shared ooxml/ reader. Reads slide order from
83
+ * `ppt/presentation.xml` (`<p:sldIdLst>`), resolves each slide part via
84
+ * relationships, and converts each slide to: an H2 of the title placeholder
85
+ * (or "Slide N"), the remaining text as a bullet list, and any slide tables
86
+ * (`<a:tbl>`) as markdown tables. Text lives in the DrawingML namespace
87
+ * (`a:p` / `a:r` / `a:t`) inside PresentationML shapes (`p:sp`).
88
+ */
89
+
90
+ interface PptxImportOptions {
91
+ /**
92
+ * Whether to extract embedded slide images into the document as image nodes
93
+ * (referencing `images/imageN.ext`). When false, pictures are ignored so the
94
+ * markdown never carries dangling image references with no backing container.
95
+ * `pptxToContainer` forces this on. Default: false.
96
+ */
97
+ extractImages?: boolean;
98
+ }
99
+ declare function pptxToMarkdownDoc(data: ArrayBuffer | Blob, options?: PptxImportOptions): Promise<MarkdownDocument>;
100
+ /**
101
+ * Convert a .pptx file to a ContentContainer with markdown + extracted images.
102
+ *
103
+ * The container holds the primary markdown document plus every embedded slide
104
+ * image under `images/` (e.g. `images/image1.png`). Image extraction is always
105
+ * forced on here so the markdown's image references resolve inside the
106
+ * container. Mirrors `docxToContainer`.
107
+ */
108
+ declare function pptxToContainer(data: ArrayBuffer | Blob, options?: PptxImportOptions): Promise<ContentContainer>;
109
+
78
110
  /**
79
111
  * @bendyline/squisq-formats PPTX Module
80
112
  *
@@ -84,7 +116,7 @@ declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<Array
84
116
  * Slide segmentation: each H1/H2 heading starts a new slide by default.
85
117
  * Inline formatting (bold, italic, code, links) is preserved as DrawingML runs.
86
118
  *
87
- * Import is not yet implemented.
119
+ * Includes both export and import paths.
88
120
  *
89
121
  * @example
90
122
  * ```ts
@@ -93,23 +125,8 @@ declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<Array
93
125
  */
94
126
 
95
127
  /**
96
- * Options for PPTX import (placeholder).
97
- */
98
- interface PptxImportOptions {
99
- /** Whether to extract embedded images as data URIs */
100
- extractImages?: boolean;
101
- }
102
- /**
103
- * Convert a .pptx file to a MarkdownDocument.
104
- *
105
- * @throws Error — PPTX import is not yet implemented
106
- */
107
- declare function pptxToMarkdownDoc(_data: ArrayBuffer | Blob, _options?: PptxImportOptions): Promise<MarkdownDocument>;
108
- /**
109
- * Convert a .pptx file to a squisq Doc.
110
- *
111
- * @throws Error — PPTX import is not yet implemented
128
+ * Convert a .pptx file to a squisq Doc (via the markdown model).
112
129
  */
113
- declare function pptxToDoc(_data: ArrayBuffer | Blob, _options?: PptxImportOptions): Promise<Doc>;
130
+ declare function pptxToDoc(data: ArrayBuffer | Blob, options?: PptxImportOptions): Promise<Doc>;
114
131
 
115
- export { type PptxExportOptions, type PptxImportOptions, docToPptx, markdownDocToPptx, pptxToDoc, pptxToMarkdownDoc };
132
+ export { type PptxExportOptions, type PptxImportOptions, docToPptx, markdownDocToPptx, pptxToContainer, pptxToDoc, pptxToMarkdownDoc };
@@ -1,14 +1,21 @@
1
1
  import {
2
2
  docToPptx,
3
3
  markdownDocToPptx,
4
+ pptxToContainer,
4
5
  pptxToDoc,
5
6
  pptxToMarkdownDoc
6
- } from "../chunk-VN2KEOYB.js";
7
- import "../chunk-7DEUGIOO.js";
7
+ } from "../chunk-YESS7VY3.js";
8
+ import "../chunk-6M7Z25LA.js";
9
+ import "../chunk-PN52A5AA.js";
10
+ import "../chunk-SHLPSADL.js";
8
11
  import "../chunk-U4MRIFKL.js";
12
+ import "../chunk-SSUPBUF5.js";
13
+ import "../chunk-MJGRI6XR.js";
14
+ import "../chunk-DQAZR57U.js";
9
15
  export {
10
16
  docToPptx,
11
17
  markdownDocToPptx,
18
+ pptxToContainer,
12
19
  pptxToDoc,
13
20
  pptxToMarkdownDoc
14
21
  };
@@ -0,0 +1,167 @@
1
+ import { Doc } from '@bendyline/squisq/schemas';
2
+ import { MarkdownDocument } from '@bendyline/squisq/markdown';
3
+ import { ContentContainer } from '@bendyline/squisq/storage';
4
+
5
+ /**
6
+ * Format registry — shared types.
7
+ *
8
+ * The registry is the programmatic heart of `convert()`: it maps a small set of
9
+ * format ids (`md`, `docx`, `pdf`, …) to `FormatDefinition`s, each of which
10
+ * knows how to import to / export from squisq's markdown + Doc model. Every
11
+ * converter module is loaded lazily via `import()` inside the definition
12
+ * methods, so pulling in the registry never eagerly bundles heavy converters.
13
+ */
14
+
15
+ /** A format identifier (e.g. `'docx'`). Strings so hosts can register their own. */
16
+ type FormatId = string;
17
+ /** The built-in formats the default registry ships with. */
18
+ declare const BUILTIN_FORMAT_IDS: readonly ["md", "docx", "pdf", "pptx", "xlsx", "csv", "html", "htmlzip", "epub", "dbk"];
19
+ /** The bytes + metadata produced by a successful export. */
20
+ interface ConversionResult {
21
+ /** Encoded output bytes. */
22
+ bytes: Uint8Array;
23
+ /** MIME type of the output. */
24
+ mimeType: string;
25
+ /** A sensible download filename (`<baseName>.<ext>`). */
26
+ suggestedFilename: string;
27
+ /** Non-fatal notes accumulated during conversion (may be empty). */
28
+ warnings: string[];
29
+ }
30
+ /**
31
+ * A source normalized into every shape an exporter might need. `doc` is always
32
+ * present; `markdownDoc` is present when the source was markdown-shaped (an
33
+ * exporter that wants markdown but finds none derives it from `doc`).
34
+ */
35
+ interface NormalizedInput {
36
+ doc: Doc;
37
+ markdownDoc?: MarkdownDocument;
38
+ container: ContentContainer;
39
+ baseName: string;
40
+ }
41
+ /** Options threaded through `convert()` and into every format method. */
42
+ interface ConvertOptions {
43
+ /** Registry to resolve formats against. Defaults to `defaultRegistry()`. */
44
+ registry?: FormatRegistry;
45
+ /** Explicit source format id (skips extension/byte sniffing). */
46
+ from?: FormatId;
47
+ /** Theme id to apply to the exported document. */
48
+ themeId?: string;
49
+ /** Transform style id to apply before export. */
50
+ transformStyle?: string;
51
+ /** Content-aware auto-templating when deriving a Doc from markdown. */
52
+ autoTemplates?: boolean;
53
+ /** Title hint for exporters that support one (epub, html). */
54
+ title?: string;
55
+ /** Lazily resolve the standalone player IIFE bundle (required for HTML export). */
56
+ resolvePlayerScript?: () => Promise<string>;
57
+ /** Per-format escape hatch for extra options. */
58
+ formatOptions?: Record<FormatId, Record<string, unknown>>;
59
+ }
60
+ /** Describes how a single format imports to / exports from the squisq model. */
61
+ interface FormatDefinition {
62
+ id: FormatId;
63
+ label: string;
64
+ mimeType: string;
65
+ extensions: readonly string[];
66
+ /** Import raw bytes to a MarkdownDocument. */
67
+ importDoc?(data: ArrayBuffer, options: ConvertOptions): Promise<MarkdownDocument>;
68
+ /** Import raw bytes to a ContentContainer (markdown + extracted media). */
69
+ importContainer?(data: ArrayBuffer, options: ConvertOptions): Promise<ContentContainer>;
70
+ /** Export a normalized input to bytes. */
71
+ exportDoc?(input: NormalizedInput, options: ConvertOptions): Promise<ConversionResult>;
72
+ }
73
+ /** A mutable collection of format definitions keyed by id. */
74
+ interface FormatRegistry {
75
+ register(def: FormatDefinition): void;
76
+ get(id: FormatId): FormatDefinition | undefined;
77
+ byExtension(ext: string): FormatDefinition | undefined;
78
+ list(): FormatDefinition[];
79
+ }
80
+ /** The three shapes `convert()` accepts as a source. */
81
+ type ConvertSource = {
82
+ kind: 'bytes';
83
+ data: ArrayBuffer | Uint8Array;
84
+ filename?: string;
85
+ } | {
86
+ kind: 'markdown';
87
+ markdown: string | MarkdownDocument;
88
+ container?: ContentContainer;
89
+ baseName?: string;
90
+ } | {
91
+ kind: 'doc';
92
+ doc: Doc;
93
+ container?: ContentContainer;
94
+ baseName?: string;
95
+ };
96
+
97
+ /**
98
+ * Structured error for the format registry / `convert()` pipeline.
99
+ *
100
+ * Every failure path in the registry throws a `ConversionError` with a stable
101
+ * machine-readable `code`, so callers can branch on the failure kind rather
102
+ * than string-matching messages.
103
+ */
104
+
105
+ /** Machine-readable failure kinds surfaced by the registry / `convert()`. */
106
+ type ConversionErrorCode = 'unknown-format' | 'unsupported-input' | 'unsupported-output' | 'invalid-input' | 'missing-dependency' | 'conversion-failed';
107
+ /** Options for constructing a {@link ConversionError}. */
108
+ interface ConversionErrorOptions {
109
+ /** The format id the error relates to, if any. */
110
+ format?: FormatId;
111
+ /** A human-oriented remediation hint. */
112
+ hint?: string;
113
+ /** The underlying error, preserved on `error.cause`. */
114
+ cause?: unknown;
115
+ }
116
+ /** An error raised by the format registry or `convert()`. */
117
+ declare class ConversionError extends Error {
118
+ readonly code: ConversionErrorCode;
119
+ readonly format?: FormatId;
120
+ readonly hint?: string;
121
+ constructor(code: ConversionErrorCode, message: string, opts?: ConversionErrorOptions);
122
+ }
123
+
124
+ /**
125
+ * Format registry implementation.
126
+ *
127
+ * `createRegistry()` returns an empty, mutable registry (last-write-wins by id);
128
+ * `defaultRegistry()` returns one preloaded with every built-in format.
129
+ */
130
+
131
+ /** Create an empty registry. Registering an existing id overwrites it. */
132
+ declare function createRegistry(): FormatRegistry;
133
+ /** Create a registry preloaded with all built-in formats. */
134
+ declare function defaultRegistry(): FormatRegistry;
135
+
136
+ /**
137
+ * Built-in format definitions.
138
+ *
139
+ * Each definition loads its converter module lazily via `import()` inside the
140
+ * method body, so importing the registry never eagerly bundles a heavy
141
+ * converter (pdf-lib, jszip, the OOXML writers, …). The capability of each
142
+ * format — which of import/export it supports — is expressed purely by which
143
+ * methods are present.
144
+ */
145
+
146
+ declare function defaultFormats(): FormatDefinition[];
147
+
148
+ /**
149
+ * Programmatic `convert()` — the format-registry front door.
150
+ *
151
+ * Normalizes any {@link ConvertSource} into a {@link NormalizedInput} (always
152
+ * yielding a `Doc`, keeping the `MarkdownDocument` when the source was
153
+ * markdown-shaped, and always carrying a `ContentContainer` for media), applies
154
+ * an optional transform, then hands off to the target format's exporter.
155
+ */
156
+
157
+ /**
158
+ * Convert a source document to a target format.
159
+ *
160
+ * @param source - A bytes / markdown / doc source.
161
+ * @param to - The target format id (must be registered and support export).
162
+ * @param options - Conversion options (registry, theme, transform, …).
163
+ * @throws {@link ConversionError} on any failure, with a stable `code`.
164
+ */
165
+ declare function convert(source: ConvertSource, to: FormatId, options?: ConvertOptions): Promise<ConversionResult>;
166
+
167
+ export { BUILTIN_FORMAT_IDS, ConversionError, type ConversionErrorCode, type ConversionErrorOptions, type ConversionResult, type ConvertOptions, type ConvertSource, type FormatDefinition, type FormatId, type FormatRegistry, type NormalizedInput, convert, createRegistry, defaultFormats, defaultRegistry };
@@ -0,0 +1,17 @@
1
+ import {
2
+ BUILTIN_FORMAT_IDS,
3
+ ConversionError,
4
+ convert,
5
+ createRegistry,
6
+ defaultFormats,
7
+ defaultRegistry
8
+ } from "../chunk-RTS5XBZ7.js";
9
+ export {
10
+ BUILTIN_FORMAT_IDS,
11
+ ConversionError,
12
+ convert,
13
+ createRegistry,
14
+ defaultFormats,
15
+ defaultRegistry
16
+ };
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,58 +1,89 @@
1
- import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
1
  import { Doc } from '@bendyline/squisq/schemas';
2
+ import { MarkdownDocument } from '@bendyline/squisq/markdown';
3
3
 
4
4
  /**
5
- * @bendyline/squisq-formats XLSX Module (Stub)
5
+ * XLSX import — SpreadsheetML (.xlsx) → MarkdownDocument.
6
6
  *
7
- * Placeholder for Excel .xlsx import/export support.
8
- * Will use SpreadsheetML (`<spreadsheet>`, `<worksheet>`, `<sheetData>`)
9
- * via the shared ooxml/ infrastructure.
7
+ * Reuses the shared ooxml/ reader (zip + DOMParser). Reads the workbook's
8
+ * sheet list, resolves each sheet part via relationships, pulls shared strings,
9
+ * and turns each worksheet's cell grid into a markdown table (first row treated
10
+ * as the header). By default every sheet is imported, each preceded by an H1 of
11
+ * the sheet name; pass `options.sheet` (index or name) to import just one.
12
+ */
13
+
14
+ interface XlsxImportOptions {
15
+ /** Which sheet to import (0-based index or sheet name). Default: all sheets. */
16
+ sheet?: number | string;
17
+ }
18
+ declare function xlsxToMarkdownDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<MarkdownDocument>;
19
+
20
+ /**
21
+ * XLSX export — MarkdownDocument → SpreadsheetML (.xlsx).
22
+ *
23
+ * Tables-only fidelity (honestly documented): every `table` node in the
24
+ * markdown AST becomes one worksheet; all other content (headings, prose,
25
+ * lists, images, …) is dropped except that the nearest preceding heading is
26
+ * used to name the sheet. This mirrors the import side (`xlsxToMarkdownDoc`),
27
+ * which turns each worksheet grid back into a markdown table.
28
+ *
29
+ * Cells are emitted as inline strings (`t="inlineStr"`) so no sharedStrings
30
+ * part is needed, except values matching a plain-number pattern, which are
31
+ * emitted as numeric cells. The package is assembled with the shared ooxml/
32
+ * writer (auto-generates `[Content_Types].xml` + `_rels`), so only the
33
+ * SpreadsheetML-specific parts (workbook, worksheets, styles) are written here.
10
34
  *
11
35
  * @example
12
36
  * ```ts
37
+ * import { parseMarkdown } from '@bendyline/squisq/markdown';
13
38
  * import { markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
39
+ *
40
+ * const md = parseMarkdown('# Metrics\n\n| A | B |\n| - | - |\n| 1 | 2 |');
41
+ * const buffer = await markdownDocToXlsx(md);
14
42
  * ```
15
43
  */
16
44
 
17
45
  /**
18
- * Options for XLSX export (placeholder).
46
+ * Options for XLSX export.
19
47
  */
20
48
  interface XlsxExportOptions {
21
- /** Workbook title */
49
+ /** Workbook title (written to core properties). */
22
50
  title?: string;
23
- /** Workbook author */
51
+ /** Workbook author (written to core properties). */
24
52
  author?: string;
53
+ /** Prefix used for auto-named sheets when no heading precedes a table. Default: "Sheet". */
54
+ sheetNamePrefix?: string;
25
55
  }
26
56
  /**
27
- * Options for XLSX import (placeholder).
28
- */
29
- interface XlsxImportOptions {
30
- /** Which sheet to import (0-based index or name). Default: 0 */
31
- sheet?: number | string;
32
- }
33
- /**
34
- * Convert a MarkdownDocument to a .xlsx Blob.
57
+ * Convert a MarkdownDocument to a .xlsx file (tables-only fidelity).
35
58
  *
36
- * @throws Error XLSX support is not yet implemented
59
+ * Each markdown `table` becomes one worksheet; a document with no tables
60
+ * yields a single empty sheet (a valid, openable file — never throws).
37
61
  */
38
- declare function markdownDocToXlsx(_doc: MarkdownDocument, _options?: XlsxExportOptions): Promise<Blob>;
62
+ declare function markdownDocToXlsx(doc: MarkdownDocument, options?: XlsxExportOptions): Promise<ArrayBuffer>;
39
63
  /**
40
- * Convert a squisq Doc to a .xlsx Blob.
41
- *
42
- * @throws Error — XLSX support is not yet implemented
64
+ * Convert a squisq Doc to a .xlsx file (via the markdown table model).
43
65
  */
44
- declare function docToXlsx(_doc: Doc, _options?: XlsxExportOptions): Promise<Blob>;
66
+ declare function docToXlsx(doc: Doc, options?: XlsxExportOptions): Promise<ArrayBuffer>;
67
+
45
68
  /**
46
- * Convert a .xlsx file to a MarkdownDocument.
69
+ * @bendyline/squisq-formats XLSX Module
47
70
  *
48
- * @throws Error XLSX support is not yet implemented
71
+ * Excel .xlsx support via SpreadsheetML and the shared ooxml/ infrastructure.
72
+ * Both directions are implemented: import (`xlsxToMarkdownDoc` / `xlsxToDoc`)
73
+ * turns each worksheet grid into a markdown table; export (`markdownDocToXlsx`
74
+ * / `docToXlsx`) turns each markdown table into a worksheet. Export is
75
+ * tables-only fidelity — non-table content is dropped (headings only survive
76
+ * as sheet names). See export.ts for details.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { xlsxToMarkdownDoc, markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
81
+ * ```
49
82
  */
50
- declare function xlsxToMarkdownDoc(_data: ArrayBuffer | Blob, _options?: XlsxImportOptions): Promise<MarkdownDocument>;
83
+
51
84
  /**
52
- * Convert a .xlsx file to a squisq Doc.
53
- *
54
- * @throws Error — XLSX support is not yet implemented
85
+ * Convert a .xlsx file to a squisq Doc (via the markdown table model).
55
86
  */
56
- declare function xlsxToDoc(_data: ArrayBuffer | Blob, _options?: XlsxImportOptions): Promise<Doc>;
87
+ declare function xlsxToDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<Doc>;
57
88
 
58
89
  export { type XlsxExportOptions, type XlsxImportOptions, docToXlsx, markdownDocToXlsx, xlsxToDoc, xlsxToMarkdownDoc };
@@ -3,7 +3,10 @@ import {
3
3
  markdownDocToXlsx,
4
4
  xlsxToDoc,
5
5
  xlsxToMarkdownDoc
6
- } from "../chunk-67KIJHV2.js";
6
+ } from "../chunk-EHLGMSTE.js";
7
+ import "../chunk-PN52A5AA.js";
8
+ import "../chunk-SHLPSADL.js";
9
+ import "../chunk-U4MRIFKL.js";
7
10
  export {
8
11
  docToXlsx,
9
12
  markdownDocToXlsx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -52,6 +52,11 @@
52
52
  "import": "./dist/xlsx/index.js",
53
53
  "default": "./dist/xlsx/index.js"
54
54
  },
55
+ "./csv": {
56
+ "types": "./dist/csv/index.d.ts",
57
+ "import": "./dist/csv/index.js",
58
+ "default": "./dist/csv/index.js"
59
+ },
55
60
  "./ooxml": {
56
61
  "types": "./dist/ooxml/index.d.ts",
57
62
  "import": "./dist/ooxml/index.js",
@@ -76,6 +81,11 @@
76
81
  "types": "./dist/container/index.d.ts",
77
82
  "import": "./dist/container/index.js",
78
83
  "default": "./dist/container/index.js"
84
+ },
85
+ "./registry": {
86
+ "types": "./dist/registry/index.d.ts",
87
+ "import": "./dist/registry/index.js",
88
+ "default": "./dist/registry/index.js"
79
89
  }
80
90
  },
81
91
  "scripts": {
@@ -84,7 +94,7 @@
84
94
  "typecheck": "tsc --noEmit"
85
95
  },
86
96
  "dependencies": {
87
- "@bendyline/squisq": "1.4.1",
97
+ "@bendyline/squisq": "1.5.1",
88
98
  "jszip": "3.10.1",
89
99
  "pdf-lib": "1.17.1",
90
100
  "pdfjs-dist": "4.10.38"
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Tests for the programmatic convert() entry point: normalization across the
3
+ * three source shapes, byte sniffing, transform threading, round-trips, and
4
+ * suggested-filename correctness.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { parseMarkdown } from '@bendyline/squisq/markdown';
9
+ import { markdownToDoc } from '@bendyline/squisq/doc';
10
+ import { convert, ConversionError, defaultRegistry } from '../registry/index';
11
+
12
+ const SAMPLE_MD = `# Round Trip
13
+
14
+ This is a paragraph that should survive a docx round trip.
15
+ `;
16
+
17
+ // ── unknown-format / unsupported-output ─────────────────────────────
18
+
19
+ describe('convert error paths', () => {
20
+ it('throws unknown-format for an unregistered target', async () => {
21
+ await expect(convert({ kind: 'markdown', markdown: '# hi' }, 'nope')).rejects.toMatchObject({
22
+ name: 'ConversionError',
23
+ code: 'unknown-format',
24
+ });
25
+ });
26
+
27
+ it('throws unsupported-output when exporting to an import-only format', async () => {
28
+ // Register a definition with only importDoc, then export to it.
29
+ const registry = defaultRegistry();
30
+ registry.register({
31
+ id: 'importonly',
32
+ label: 'Import Only',
33
+ mimeType: 'application/x-import',
34
+ extensions: ['.imp'],
35
+ async importDoc() {
36
+ return parseMarkdown('# x');
37
+ },
38
+ });
39
+ await expect(
40
+ convert({ kind: 'markdown', markdown: '# hi' }, 'importonly', { registry }),
41
+ ).rejects.toMatchObject({ name: 'ConversionError', code: 'unsupported-output' });
42
+ });
43
+ });
44
+
45
+ // ── round trip: md → docx → md ──────────────────────────────────────
46
+
47
+ describe('md → docx → md round trip', () => {
48
+ it('preserves paragraph text through a docx round trip', async () => {
49
+ const toDocx = await convert(
50
+ { kind: 'markdown', markdown: SAMPLE_MD, baseName: 'doc' },
51
+ 'docx',
52
+ );
53
+ expect(toDocx.mimeType).toContain('wordprocessingml');
54
+ expect(toDocx.bytes.byteLength).toBeGreaterThan(0);
55
+
56
+ // Feed the docx bytes back in with an explicit `from`.
57
+ const backToMd = await convert(
58
+ { kind: 'bytes', data: toDocx.bytes, filename: 'doc.docx' },
59
+ 'md',
60
+ { from: 'docx' },
61
+ );
62
+ const text = new TextDecoder().decode(backToMd.bytes);
63
+ expect(text).toContain('survive a docx round trip');
64
+ expect(backToMd.suggestedFilename).toBe('doc.md');
65
+ });
66
+ });
67
+
68
+ // ── byte sniffing ───────────────────────────────────────────────────
69
+
70
+ describe('byte sniffing', () => {
71
+ it('sniffs a PDF by its %PDF magic', async () => {
72
+ // Stub the pdf importer so the test exercises the sniff, not pdfjs (which
73
+ // can't run under jsdom). Any %PDF-prefixed bytes must route to 'pdf'.
74
+ const registry = defaultRegistry();
75
+ registry.register({
76
+ ...registry.get('pdf')!,
77
+ importContainer: undefined, // force the importDoc path (skip pdfjs)
78
+ async importDoc() {
79
+ return parseMarkdown('# sniffed as pdf');
80
+ },
81
+ });
82
+ const pdfBytes = new TextEncoder().encode('%PDF-1.4\n%mock pdf body\n');
83
+ const back = await convert({ kind: 'bytes', data: pdfBytes }, 'md', { registry });
84
+ expect(new TextDecoder().decode(back.bytes)).toContain('sniffed as pdf');
85
+ });
86
+
87
+ it('sniffs a squisq container (plain zip, no [Content_Types].xml) as dbk', async () => {
88
+ // A dbk is a ZIP with a markdown doc and no OOXML content-types part.
89
+ const dbk = await convert({ kind: 'markdown', markdown: '# In a container\n\nBody.' }, 'dbk');
90
+ expect(dbk.mimeType).toBe('application/zip');
91
+
92
+ const back = await convert({ kind: 'bytes', data: dbk.bytes }, 'md');
93
+ const text = new TextDecoder().decode(back.bytes);
94
+ expect(text).toContain('In a container');
95
+ });
96
+
97
+ it('disambiguates a docx zip via its content-types part', async () => {
98
+ const docx = await convert({ kind: 'markdown', markdown: '# Word doc' }, 'docx');
99
+ // No filename, no `from` — must read [Content_Types].xml to pick docx.
100
+ const back = await convert({ kind: 'bytes', data: docx.bytes }, 'md');
101
+ const text = new TextDecoder().decode(back.bytes);
102
+ expect(text).toContain('Word doc');
103
+ });
104
+ });
105
+
106
+ // ── transform threading ─────────────────────────────────────────────
107
+
108
+ describe('transform threading', () => {
109
+ it('applies a transform style before handing off to the exporter', async () => {
110
+ // A capture format records the NormalizedInput it receives, letting us
111
+ // observe that the transform ran without depending on any converter.
112
+ let captured: import('../registry/index').NormalizedInput | undefined;
113
+ const registry = defaultRegistry();
114
+ registry.register({
115
+ id: 'capture',
116
+ label: 'Capture',
117
+ mimeType: 'application/x-capture',
118
+ extensions: ['.cap'],
119
+ async exportDoc(input) {
120
+ captured = input;
121
+ return {
122
+ bytes: new Uint8Array([1]),
123
+ mimeType: 'application/x-capture',
124
+ suggestedFilename: '',
125
+ warnings: [],
126
+ };
127
+ },
128
+ });
129
+
130
+ const plainDoc = markdownToDoc(parseMarkdown(SAMPLE_MD));
131
+ await convert({ kind: 'markdown', markdown: SAMPLE_MD }, 'capture', {
132
+ registry,
133
+ transformStyle: 'magazine',
134
+ });
135
+
136
+ expect(captured).toBeDefined();
137
+ // The transform promotes/reshapes blocks, so the exporter sees a different
138
+ // Doc than a plain markdownToDoc of the same source.
139
+ expect(captured!.doc.blocks.length).not.toBe(plainDoc.blocks.length);
140
+ // markdownDoc is re-derived from the transformed Doc.
141
+ expect(captured!.markdownDoc).toBeDefined();
142
+ });
143
+ });
144
+
145
+ // ── doc-kind source ─────────────────────────────────────────────────
146
+
147
+ describe('doc-kind source', () => {
148
+ it('exports a Doc source', async () => {
149
+ const doc = markdownToDoc(parseMarkdown('# From a Doc\n\nContent.'));
150
+ const result = await convert({ kind: 'doc', doc, baseName: 'fromdoc' }, 'md');
151
+ const text = new TextDecoder().decode(result.bytes);
152
+ expect(text).toContain('From a Doc');
153
+ expect(result.suggestedFilename).toBe('fromdoc.md');
154
+ });
155
+ });
156
+
157
+ // ── suggested filename ──────────────────────────────────────────────
158
+
159
+ describe('suggestedFilename', () => {
160
+ it('uses the primary extension per format', async () => {
161
+ const docx = await convert({ kind: 'markdown', markdown: '# t', baseName: 'report' }, 'docx');
162
+ expect(docx.suggestedFilename).toBe('report.docx');
163
+ });
164
+
165
+ it('htmlzip suggests <base>.html.zip', async () => {
166
+ const zip = await convert(
167
+ { kind: 'markdown', markdown: '# t', baseName: 'slides' },
168
+ 'htmlzip',
169
+ {
170
+ resolvePlayerScript: async () => '/* stub */',
171
+ },
172
+ );
173
+ expect(zip.suggestedFilename).toBe('slides.html.zip');
174
+ expect(zip.mimeType).toBe('application/zip');
175
+ });
176
+
177
+ it('defaults baseName to "document" when none is given', async () => {
178
+ const out = await convert({ kind: 'markdown', markdown: '# t' }, 'md');
179
+ expect(out.suggestedFilename).toBe('document.md');
180
+ });
181
+ });
182
+
183
+ // sanity: ConversionError is exported and usable
184
+ it('exports ConversionError', () => {
185
+ expect(new ConversionError('conversion-failed', 'x')).toBeInstanceOf(Error);
186
+ });