@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
@@ -1,14 +1,3 @@
1
- import {
2
- createPackage
3
- } from "../chunk-QSE7EWE7.js";
4
- import {
5
- attrString,
6
- escapeXml,
7
- selfClosingElement,
8
- textElement,
9
- xmlDeclaration,
10
- xmlElement
11
- } from "../chunk-U4MRIFKL.js";
12
1
  import {
13
2
  CONTENT_TYPE_CORE_PROPERTIES,
14
3
  CONTENT_TYPE_DOCX_DOCUMENT,
@@ -23,7 +12,9 @@ import {
23
12
  CONTENT_TYPE_PPTX_SLIDE_MASTER,
24
13
  CONTENT_TYPE_PPTX_THEME,
25
14
  CONTENT_TYPE_RELATIONSHIPS,
15
+ CONTENT_TYPE_XLSX_STYLES,
26
16
  CONTENT_TYPE_XLSX_WORKBOOK,
17
+ CONTENT_TYPE_XLSX_WORKSHEET,
27
18
  NS_CONTENT_TYPES,
28
19
  NS_CORE_PROPERTIES,
29
20
  NS_DC,
@@ -52,12 +43,22 @@ import {
52
43
  REL_SLIDE_MASTER,
53
44
  REL_STYLES,
54
45
  REL_THEME,
46
+ REL_WORKSHEET,
47
+ createPackage,
55
48
  getCoreProperties,
56
49
  getPartBinary,
57
50
  getPartRelationships,
58
51
  getPartXml,
59
52
  openPackage
60
- } from "../chunk-FMOIGA5T.js";
53
+ } from "../chunk-SHLPSADL.js";
54
+ import {
55
+ attrString,
56
+ escapeXml,
57
+ selfClosingElement,
58
+ textElement,
59
+ xmlDeclaration,
60
+ xmlElement
61
+ } from "../chunk-U4MRIFKL.js";
61
62
  export {
62
63
  CONTENT_TYPE_CORE_PROPERTIES,
63
64
  CONTENT_TYPE_DOCX_DOCUMENT,
@@ -72,7 +73,9 @@ export {
72
73
  CONTENT_TYPE_PPTX_SLIDE_MASTER,
73
74
  CONTENT_TYPE_PPTX_THEME,
74
75
  CONTENT_TYPE_RELATIONSHIPS,
76
+ CONTENT_TYPE_XLSX_STYLES,
75
77
  CONTENT_TYPE_XLSX_WORKBOOK,
78
+ CONTENT_TYPE_XLSX_WORKSHEET,
76
79
  NS_CONTENT_TYPES,
77
80
  NS_CORE_PROPERTIES,
78
81
  NS_DC,
@@ -101,6 +104,7 @@ export {
101
104
  REL_SLIDE_MASTER,
102
105
  REL_STYLES,
103
106
  REL_THEME,
107
+ REL_WORKSHEET,
104
108
  attrString,
105
109
  createPackage,
106
110
  escapeXml,
package/dist/pdf/index.js CHANGED
@@ -5,7 +5,8 @@ import {
5
5
  pdfToContainer,
6
6
  pdfToDoc,
7
7
  pdfToMarkdownDoc
8
- } from "../chunk-APSIAEXL.js";
8
+ } from "../chunk-D7IFPWNZ.js";
9
+ import "../chunk-SSUPBUF5.js";
9
10
  export {
10
11
  configurePdfWorker,
11
12
  docToPdf,
@@ -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
@@ -87,10 +88,24 @@ declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<Array
87
88
  */
88
89
 
89
90
  interface PptxImportOptions {
90
- /** Whether to extract embedded images as data URIs (not yet used). */
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
+ */
91
97
  extractImages?: boolean;
92
98
  }
93
- declare function pptxToMarkdownDoc(data: ArrayBuffer | Blob, _options?: PptxImportOptions): Promise<MarkdownDocument>;
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>;
94
109
 
95
110
  /**
96
111
  * @bendyline/squisq-formats PPTX Module
@@ -114,4 +129,4 @@ declare function pptxToMarkdownDoc(data: ArrayBuffer | Blob, _options?: PptxImpo
114
129
  */
115
130
  declare function pptxToDoc(data: ArrayBuffer | Blob, options?: PptxImportOptions): Promise<Doc>;
116
131
 
117
- export { type PptxExportOptions, type PptxImportOptions, docToPptx, markdownDocToPptx, pptxToDoc, pptxToMarkdownDoc };
132
+ export { type PptxExportOptions, type PptxImportOptions, docToPptx, markdownDocToPptx, pptxToContainer, pptxToDoc, pptxToMarkdownDoc };
@@ -1,18 +1,21 @@
1
1
  import {
2
2
  docToPptx,
3
3
  markdownDocToPptx,
4
+ pptxToContainer,
4
5
  pptxToDoc,
5
6
  pptxToMarkdownDoc
6
- } from "../chunk-I2GFTEOV.js";
7
+ } from "../chunk-YESS7VY3.js";
7
8
  import "../chunk-6M7Z25LA.js";
8
9
  import "../chunk-PN52A5AA.js";
9
- import "../chunk-QSE7EWE7.js";
10
+ import "../chunk-SHLPSADL.js";
10
11
  import "../chunk-U4MRIFKL.js";
11
- import "../chunk-FMOIGA5T.js";
12
- import "../chunk-A3FHLTY5.js";
12
+ import "../chunk-SSUPBUF5.js";
13
+ import "../chunk-MJGRI6XR.js";
14
+ import "../chunk-DQAZR57U.js";
13
15
  export {
14
16
  docToPptx,
15
17
  markdownDocToPptx,
18
+ pptxToContainer,
16
19
  pptxToDoc,
17
20
  pptxToMarkdownDoc
18
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,5 +1,5 @@
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
5
  * XLSX import — SpreadsheetML (.xlsx) → MarkdownDocument.
@@ -18,39 +18,69 @@ interface XlsxImportOptions {
18
18
  declare function xlsxToMarkdownDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<MarkdownDocument>;
19
19
 
20
20
  /**
21
- * @bendyline/squisq-formats XLSX Module (Stub)
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.
22
28
  *
23
- * Placeholder for Excel .xlsx import/export support.
24
- * Will use SpreadsheetML (`<spreadsheet>`, `<worksheet>`, `<sheetData>`)
25
- * via the shared ooxml/ infrastructure.
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.
26
34
  *
27
35
  * @example
28
36
  * ```ts
37
+ * import { parseMarkdown } from '@bendyline/squisq/markdown';
29
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);
30
42
  * ```
31
43
  */
32
44
 
33
45
  /**
34
- * Options for XLSX export (placeholder).
46
+ * Options for XLSX export.
35
47
  */
36
48
  interface XlsxExportOptions {
37
- /** Workbook title */
49
+ /** Workbook title (written to core properties). */
38
50
  title?: string;
39
- /** Workbook author */
51
+ /** Workbook author (written to core properties). */
40
52
  author?: string;
53
+ /** Prefix used for auto-named sheets when no heading precedes a table. Default: "Sheet". */
54
+ sheetNamePrefix?: string;
41
55
  }
42
56
  /**
43
- * Convert a MarkdownDocument to a .xlsx Blob.
57
+ * Convert a MarkdownDocument to a .xlsx file (tables-only fidelity).
44
58
  *
45
- * @throws Error XLSX export 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).
61
+ */
62
+ declare function markdownDocToXlsx(doc: MarkdownDocument, options?: XlsxExportOptions): Promise<ArrayBuffer>;
63
+ /**
64
+ * Convert a squisq Doc to a .xlsx file (via the markdown table model).
46
65
  */
47
- declare function markdownDocToXlsx(_doc: MarkdownDocument, _options?: XlsxExportOptions): Promise<Blob>;
66
+ declare function docToXlsx(doc: Doc, options?: XlsxExportOptions): Promise<ArrayBuffer>;
67
+
48
68
  /**
49
- * Convert a squisq Doc to a .xlsx Blob.
69
+ * @bendyline/squisq-formats XLSX Module
70
+ *
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.
50
77
  *
51
- * @throws Error — XLSX export is not yet implemented
78
+ * @example
79
+ * ```ts
80
+ * import { xlsxToMarkdownDoc, markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
81
+ * ```
52
82
  */
53
- declare function docToXlsx(_doc: Doc, _options?: XlsxExportOptions): Promise<Blob>;
83
+
54
84
  /**
55
85
  * Convert a .xlsx file to a squisq Doc (via the markdown table model).
56
86
  */
@@ -3,8 +3,10 @@ import {
3
3
  markdownDocToXlsx,
4
4
  xlsxToDoc,
5
5
  xlsxToMarkdownDoc
6
- } from "../chunk-L53YIGYS.js";
7
- import "../chunk-FMOIGA5T.js";
6
+ } from "../chunk-EHLGMSTE.js";
7
+ import "../chunk-PN52A5AA.js";
8
+ import "../chunk-SHLPSADL.js";
9
+ import "../chunk-U4MRIFKL.js";
8
10
  export {
9
11
  docToXlsx,
10
12
  markdownDocToXlsx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -81,6 +81,11 @@
81
81
  "types": "./dist/container/index.d.ts",
82
82
  "import": "./dist/container/index.js",
83
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"
84
89
  }
85
90
  },
86
91
  "scripts": {
@@ -89,7 +94,7 @@
89
94
  "typecheck": "tsc --noEmit"
90
95
  },
91
96
  "dependencies": {
92
- "@bendyline/squisq": "1.5.0",
97
+ "@bendyline/squisq": "1.5.1",
93
98
  "jszip": "3.10.1",
94
99
  "pdf-lib": "1.17.1",
95
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
+ });