@bendyline/squisq-formats 1.3.1 → 1.4.0

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 (76) hide show
  1. package/dist/chunk-6M7Z25LA.js +46 -0
  2. package/dist/chunk-6M7Z25LA.js.map +1 -0
  3. package/dist/{chunk-S3Y7H2BK.js → chunk-APSIAEXL.js} +9 -8
  4. package/dist/chunk-APSIAEXL.js.map +1 -0
  5. package/dist/{chunk-UDS45KUJ.js → chunk-BJQIJ4S4.js} +36 -52
  6. package/dist/chunk-BJQIJ4S4.js.map +1 -0
  7. package/dist/{chunk-33YRFXZZ.js → chunk-CRTC6DEX.js} +259 -13
  8. package/dist/chunk-CRTC6DEX.js.map +1 -0
  9. package/dist/chunk-FMOIGA5T.js +201 -0
  10. package/dist/chunk-FMOIGA5T.js.map +1 -0
  11. package/dist/{chunk-VN2KEOYB.js → chunk-I2GFTEOV.js} +321 -91
  12. package/dist/chunk-I2GFTEOV.js.map +1 -0
  13. package/dist/chunk-JBNVE2GV.js +116 -0
  14. package/dist/chunk-JBNVE2GV.js.map +1 -0
  15. package/dist/chunk-L53YIGYS.js +141 -0
  16. package/dist/chunk-L53YIGYS.js.map +1 -0
  17. package/dist/{chunk-ERZ627GR.js → chunk-ON232HJR.js} +11 -26
  18. package/dist/chunk-ON232HJR.js.map +1 -0
  19. package/dist/chunk-PN52A5AA.js +32 -0
  20. package/dist/chunk-PN52A5AA.js.map +1 -0
  21. package/dist/{chunk-7DEUGIOO.js → chunk-QSE7EWE7.js} +12 -87
  22. package/dist/chunk-QSE7EWE7.js.map +1 -0
  23. package/dist/csv/index.d.ts +37 -0
  24. package/dist/csv/index.js +13 -0
  25. package/dist/csv/index.js.map +1 -0
  26. package/dist/docx/index.js +5 -3
  27. package/dist/epub/index.js +2 -1
  28. package/dist/html/index.d.ts +39 -2
  29. package/dist/html/index.js +7 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +31 -13
  32. package/dist/ooxml/index.js +16 -16
  33. package/dist/pdf/index.js +1 -1
  34. package/dist/pptx/index.d.ts +20 -18
  35. package/dist/pptx/index.js +6 -2
  36. package/dist/xlsx/index.d.ts +20 -19
  37. package/dist/xlsx/index.js +2 -1
  38. package/package.json +7 -2
  39. package/src/__tests__/csvImport.test.ts +51 -0
  40. package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
  41. package/src/__tests__/htmlImport.test.ts +57 -0
  42. package/src/__tests__/plainHtml.test.ts +31 -0
  43. package/src/__tests__/pptxExport.test.ts +138 -0
  44. package/src/__tests__/pptxImport.test.ts +70 -0
  45. package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
  46. package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
  47. package/src/__tests__/roundTripMatrix.test.ts +136 -0
  48. package/src/__tests__/xlsxImport.test.ts +80 -0
  49. package/src/csv/index.ts +165 -0
  50. package/src/docx/export.ts +27 -44
  51. package/src/docx/import.ts +1 -2
  52. package/src/epub/export.ts +10 -27
  53. package/src/html/import.ts +297 -0
  54. package/src/html/index.ts +4 -0
  55. package/src/html/plainHtml.ts +66 -16
  56. package/src/index.ts +10 -3
  57. package/src/ooxml/namespaces.ts +3 -0
  58. package/src/pdf/export.ts +10 -5
  59. package/src/pdf/import.ts +1 -1
  60. package/src/pptx/export.ts +225 -85
  61. package/src/pptx/import.ts +156 -0
  62. package/src/pptx/index.ts +10 -28
  63. package/src/shared/inlineRuns.ts +99 -0
  64. package/src/shared/text.ts +41 -0
  65. package/src/xlsx/import.ts +159 -0
  66. package/src/xlsx/index.ts +11 -28
  67. package/dist/chunk-33YRFXZZ.js.map +0 -1
  68. package/dist/chunk-67KIJHV2.js +0 -21
  69. package/dist/chunk-67KIJHV2.js.map +0 -1
  70. package/dist/chunk-7DEUGIOO.js.map +0 -1
  71. package/dist/chunk-ERZ627GR.js.map +0 -1
  72. package/dist/chunk-S3Y7H2BK.js.map +0 -1
  73. package/dist/chunk-UDS45KUJ.js.map +0 -1
  74. package/dist/chunk-VN2KEOYB.js.map +0 -1
  75. package/dist/chunk-YN5HFCEW.js +0 -120
  76. package/dist/chunk-YN5HFCEW.js.map +0 -1
@@ -75,6 +75,23 @@ declare function markdownDocToPptx(doc: MarkdownDocument, options?: PptxExportOp
75
75
  */
76
76
  declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<ArrayBuffer>;
77
77
 
78
+ /**
79
+ * PPTX import — PresentationML (.pptx) → MarkdownDocument.
80
+ *
81
+ * Reuses the shared ooxml/ reader. Reads slide order from
82
+ * `ppt/presentation.xml` (`<p:sldIdLst>`), resolves each slide part via
83
+ * relationships, and converts each slide to: an H2 of the title placeholder
84
+ * (or "Slide N"), the remaining text as a bullet list, and any slide tables
85
+ * (`<a:tbl>`) as markdown tables. Text lives in the DrawingML namespace
86
+ * (`a:p` / `a:r` / `a:t`) inside PresentationML shapes (`p:sp`).
87
+ */
88
+
89
+ interface PptxImportOptions {
90
+ /** Whether to extract embedded images as data URIs (not yet used). */
91
+ extractImages?: boolean;
92
+ }
93
+ declare function pptxToMarkdownDoc(data: ArrayBuffer | Blob, _options?: PptxImportOptions): Promise<MarkdownDocument>;
94
+
78
95
  /**
79
96
  * @bendyline/squisq-formats PPTX Module
80
97
  *
@@ -84,7 +101,7 @@ declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<Array
84
101
  * Slide segmentation: each H1/H2 heading starts a new slide by default.
85
102
  * Inline formatting (bold, italic, code, links) is preserved as DrawingML runs.
86
103
  *
87
- * Import is not yet implemented.
104
+ * Includes both export and import paths.
88
105
  *
89
106
  * @example
90
107
  * ```ts
@@ -93,23 +110,8 @@ declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<Array
93
110
  */
94
111
 
95
112
  /**
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
113
+ * Convert a .pptx file to a squisq Doc (via the markdown model).
112
114
  */
113
- declare function pptxToDoc(_data: ArrayBuffer | Blob, _options?: PptxImportOptions): Promise<Doc>;
115
+ declare function pptxToDoc(data: ArrayBuffer | Blob, options?: PptxImportOptions): Promise<Doc>;
114
116
 
115
117
  export { type PptxExportOptions, type PptxImportOptions, docToPptx, markdownDocToPptx, pptxToDoc, pptxToMarkdownDoc };
@@ -3,9 +3,13 @@ import {
3
3
  markdownDocToPptx,
4
4
  pptxToDoc,
5
5
  pptxToMarkdownDoc
6
- } from "../chunk-VN2KEOYB.js";
7
- import "../chunk-7DEUGIOO.js";
6
+ } from "../chunk-I2GFTEOV.js";
7
+ import "../chunk-6M7Z25LA.js";
8
+ import "../chunk-PN52A5AA.js";
9
+ import "../chunk-QSE7EWE7.js";
8
10
  import "../chunk-U4MRIFKL.js";
11
+ import "../chunk-FMOIGA5T.js";
12
+ import "../chunk-A3FHLTY5.js";
9
13
  export {
10
14
  docToPptx,
11
15
  markdownDocToPptx,
@@ -1,6 +1,22 @@
1
1
  import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
2
  import { Doc } from '@bendyline/squisq/schemas';
3
3
 
4
+ /**
5
+ * XLSX import — SpreadsheetML (.xlsx) → MarkdownDocument.
6
+ *
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
+
4
20
  /**
5
21
  * @bendyline/squisq-formats XLSX Module (Stub)
6
22
  *
@@ -23,36 +39,21 @@ interface XlsxExportOptions {
23
39
  /** Workbook author */
24
40
  author?: string;
25
41
  }
26
- /**
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
42
  /**
34
43
  * Convert a MarkdownDocument to a .xlsx Blob.
35
44
  *
36
- * @throws Error — XLSX support is not yet implemented
45
+ * @throws Error — XLSX export is not yet implemented
37
46
  */
38
47
  declare function markdownDocToXlsx(_doc: MarkdownDocument, _options?: XlsxExportOptions): Promise<Blob>;
39
48
  /**
40
49
  * Convert a squisq Doc to a .xlsx Blob.
41
50
  *
42
- * @throws Error — XLSX support is not yet implemented
51
+ * @throws Error — XLSX export is not yet implemented
43
52
  */
44
53
  declare function docToXlsx(_doc: Doc, _options?: XlsxExportOptions): Promise<Blob>;
45
54
  /**
46
- * Convert a .xlsx file to a MarkdownDocument.
47
- *
48
- * @throws Error — XLSX support is not yet implemented
49
- */
50
- declare function xlsxToMarkdownDoc(_data: ArrayBuffer | Blob, _options?: XlsxImportOptions): Promise<MarkdownDocument>;
51
- /**
52
- * Convert a .xlsx file to a squisq Doc.
53
- *
54
- * @throws Error — XLSX support is not yet implemented
55
+ * Convert a .xlsx file to a squisq Doc (via the markdown table model).
55
56
  */
56
- declare function xlsxToDoc(_data: ArrayBuffer | Blob, _options?: XlsxImportOptions): Promise<Doc>;
57
+ declare function xlsxToDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<Doc>;
57
58
 
58
59
  export { type XlsxExportOptions, type XlsxImportOptions, docToXlsx, markdownDocToXlsx, xlsxToDoc, xlsxToMarkdownDoc };
@@ -3,7 +3,8 @@ import {
3
3
  markdownDocToXlsx,
4
4
  xlsxToDoc,
5
5
  xlsxToMarkdownDoc
6
- } from "../chunk-67KIJHV2.js";
6
+ } from "../chunk-L53YIGYS.js";
7
+ import "../chunk-FMOIGA5T.js";
7
8
  export {
8
9
  docToXlsx,
9
10
  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.0",
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",
@@ -84,7 +89,7 @@
84
89
  "typecheck": "tsc --noEmit"
85
90
  },
86
91
  "dependencies": {
87
- "@bendyline/squisq": "1.4.1",
92
+ "@bendyline/squisq": "1.5.0",
88
93
  "jszip": "3.10.1",
89
94
  "pdf-lib": "1.17.1",
90
95
  "pdfjs-dist": "4.10.38"
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tests for CSV import/export: csvToMarkdownDoc, markdownDocToCsv, parseCsv.
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+ import type { MarkdownTable, MarkdownText } from '@bendyline/squisq/markdown';
7
+ import { csvToMarkdownDoc, markdownDocToCsv, parseCsv } from '../csv/index';
8
+
9
+ function cellValue(table: MarkdownTable, row: number, col: number): string {
10
+ const cell = table.children[row]?.children[col];
11
+ const first = cell?.children[0] as MarkdownText | undefined;
12
+ return first?.value ?? '';
13
+ }
14
+
15
+ describe('parseCsv', () => {
16
+ it('parses quoted fields, escaped quotes, and embedded delimiters', () => {
17
+ const rows = parseCsv('a,b,c\n"x,y","he said ""hi""",z\n');
18
+ expect(rows).toEqual([
19
+ ['a', 'b', 'c'],
20
+ ['x,y', 'he said "hi"', 'z'],
21
+ ]);
22
+ });
23
+
24
+ it('handles CRLF line endings', () => {
25
+ expect(parseCsv('a,b\r\n1,2\r\n')).toEqual([
26
+ ['a', 'b'],
27
+ ['1', '2'],
28
+ ]);
29
+ });
30
+ });
31
+
32
+ describe('csvToMarkdownDoc', () => {
33
+ it('builds a table with a header row by default', async () => {
34
+ const doc = await csvToMarkdownDoc('Name,Age\nAlice,30\n"Bob, Jr.",40\n');
35
+ expect(doc.type).toBe('document');
36
+ const table = doc.children[0] as MarkdownTable;
37
+ expect(table.type).toBe('table');
38
+ expect(table.children).toHaveLength(3);
39
+ expect(table.children[0]!.children[0]!.isHeader).toBe(true);
40
+ expect(cellValue(table, 0, 0)).toBe('Name');
41
+ expect(cellValue(table, 0, 1)).toBe('Age');
42
+ expect(cellValue(table, 2, 0)).toBe('Bob, Jr.');
43
+ expect(cellValue(table, 2, 1)).toBe('40');
44
+ });
45
+
46
+ it('round-trips through markdownDocToCsv', async () => {
47
+ const csv = 'Name,Age\r\nAlice,30\r\n"Bob, Jr.",40';
48
+ const doc = await csvToMarkdownDoc(csv);
49
+ expect(markdownDocToCsv(doc)).toBe(csv);
50
+ });
51
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Export theme reconciliation.
3
+ *
4
+ * Every export format must honor the theme the document selects in its
5
+ * frontmatter (`squisq-theme`) — including an inline custom theme stored in
6
+ * `squisq-custom-themes` — without an explicit `themeId` option. This guards
7
+ * the fix that made PDF/EPUB read the frontmatter theme like PPTX/DOCX/HTML,
8
+ * and that custom themes resolve doc-scoped in exports (no global register).
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import JSZip from 'jszip';
13
+ import { compileTheme } from '@bendyline/squisq/schemas';
14
+ import { writeCustomThemesToFrontmatter } from '@bendyline/squisq/doc';
15
+ import { parseMarkdown } from '@bendyline/squisq/markdown';
16
+ import { markdownDocToPptx } from '../pptx/export';
17
+ import { markdownDocToDocx } from '../docx/export';
18
+ import { markdownDocToPdf } from '../pdf/export';
19
+ import { markdownDocToEpub } from '../epub/export';
20
+ import { markdownDocToPlainHtml } from '../html/plainHtml';
21
+
22
+ const brand = compileTheme({
23
+ id: 'custom-brand',
24
+ name: 'Brand',
25
+ seedColors: { primary: '#ff0088', background: '#123456', text: '#abcdef' },
26
+ });
27
+
28
+ const withTheme = parseMarkdown(`---
29
+ title: Audit
30
+ squisq-theme: custom-brand
31
+ squisq-custom-themes: ${writeCustomThemesToFrontmatter([brand])}
32
+ ---
33
+
34
+ # Heading One
35
+
36
+ Body with a [link](https://example.com).
37
+ `);
38
+
39
+ const noTheme = parseMarkdown('# Heading One\n\nBody.\n');
40
+
41
+ /** Concatenate the text of every XML/CSS/HTML entry in a ZIP export. */
42
+ async function zipText(buf: ArrayBuffer): Promise<string> {
43
+ const zip = await JSZip.loadAsync(buf);
44
+ let text = '';
45
+ for (const name of Object.keys(zip.files)) {
46
+ if (/\.(xml|css|html|xhtml|rels)$/i.test(name)) {
47
+ text += await zip.files[name].async('string');
48
+ }
49
+ }
50
+ return text.toLowerCase();
51
+ }
52
+
53
+ describe('export theme reconciliation (frontmatter-only custom theme)', () => {
54
+ it('HTML applies the frontmatter custom theme colors', () => {
55
+ const html = markdownDocToPlainHtml(withTheme, {}).toLowerCase();
56
+ expect(html).toContain('123456'); // background
57
+ expect(html).toContain('abcdef'); // text
58
+ });
59
+
60
+ it('PPTX applies the frontmatter custom theme colors', async () => {
61
+ const text = await zipText(await markdownDocToPptx(withTheme, {}));
62
+ expect(text).toContain('123456');
63
+ });
64
+
65
+ it('DOCX applies the frontmatter custom theme colors', async () => {
66
+ const text = await zipText(await markdownDocToDocx(withTheme, {}));
67
+ expect(text).toContain('123456');
68
+ });
69
+
70
+ it('EPUB applies the frontmatter custom theme colors (frontmatter fallback)', async () => {
71
+ const text = await zipText(await markdownDocToEpub(withTheme, {}));
72
+ expect(text).toContain('123456');
73
+ });
74
+
75
+ it('PDF themes from frontmatter — themed output differs from un-themed', async () => {
76
+ const themed = new Uint8Array(await markdownDocToPdf(withTheme, {}));
77
+ const plain = new Uint8Array(await markdownDocToPdf(noTheme, {}));
78
+ const differs = themed.length !== plain.length || !themed.every((b, i) => b === plain[i]);
79
+ expect(differs).toBe(true);
80
+ });
81
+
82
+ it('an explicit themeId option still overrides the frontmatter theme', async () => {
83
+ const bare = await zipText(await markdownDocToPptx(withTheme, { themeId: 'standard' }));
84
+ // The custom background must NOT appear when the caller forces 'standard'.
85
+ expect(bare).not.toContain('123456');
86
+ });
87
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { htmlToMarkdown, htmlToMarkdownDocSync } from '../html/import.js';
3
+
4
+ describe('htmlToMarkdown', () => {
5
+ it('converts headings and paragraphs', () => {
6
+ const md = htmlToMarkdown('<h1>Title</h1><p>Hello <strong>world</strong>.</p>');
7
+ expect(md).toContain('# Title');
8
+ expect(md).toContain('Hello **world**.');
9
+ });
10
+
11
+ it('converts links and inline code', () => {
12
+ const md = htmlToMarkdown(
13
+ '<p>See <a href="https://x.test">site</a> and <code>npm i</code></p>',
14
+ );
15
+ expect(md).toContain('[site](https://x.test)');
16
+ expect(md).toContain('`npm i`');
17
+ });
18
+
19
+ it('converts unordered and ordered lists', () => {
20
+ const md = htmlToMarkdown('<ul><li>a</li><li>b</li></ul><ol><li>one</li><li>two</li></ol>');
21
+ expect(md).toMatch(/[-*]\s+a/);
22
+ expect(md).toMatch(/[-*]\s+b/);
23
+ expect(md).toMatch(/1\.\s+one/);
24
+ });
25
+
26
+ it('converts blockquotes and tables', () => {
27
+ const md = htmlToMarkdown(
28
+ '<blockquote><p>quoted</p></blockquote>' +
29
+ '<table><thead><tr><th>H</th></tr></thead><tbody><tr><td>cell</td></tr></tbody></table>',
30
+ );
31
+ expect(md).toContain('> quoted');
32
+ expect(md).toContain('cell');
33
+ });
34
+
35
+ it('drops scripts and styles (sanitized by default)', () => {
36
+ const md = htmlToMarkdown('<p>safe</p><script>alert(1)</script><style>.x{color:red}</style>');
37
+ expect(md).toContain('safe');
38
+ expect(md).not.toContain('alert(1)');
39
+ expect(md).not.toContain('color:red');
40
+ });
41
+
42
+ it('strips javascript: links via sanitizer', () => {
43
+ const md = htmlToMarkdown('<a href="javascript:alert(1)">click</a>');
44
+ expect(md).not.toContain('javascript:alert(1)');
45
+ });
46
+
47
+ it('builds a document node tree', () => {
48
+ const doc = htmlToMarkdownDocSync('<h2>Hi</h2>');
49
+ expect(doc.type).toBe('document');
50
+ expect(doc.children[0]?.type).toBe('heading');
51
+ });
52
+
53
+ it('handles unwrapped div/span containers', () => {
54
+ const md = htmlToMarkdown('<div><span>plain </span><em>text</em></div>');
55
+ expect(md).toContain('plain *text*');
56
+ });
57
+ });
@@ -43,6 +43,12 @@ describe('markdownDocToPlainHtml', () => {
43
43
  );
44
44
  });
45
45
 
46
+ it('renders unsafe markdown links as inert text by default', () => {
47
+ const html = render('[x](javascript:alert(1))');
48
+ expect(html).toContain('<p>x</p>');
49
+ expect(html).not.toContain('javascript:');
50
+ });
51
+
46
52
  it('renders ordered and unordered lists', () => {
47
53
  const html = render('- one\n- two\n\n1. first\n2. second');
48
54
  expect(html).toContain('<ul>');
@@ -112,6 +118,31 @@ describe('markdownDocToPlainHtml', () => {
112
118
  expect(html).not.toContain('src="resized.png"');
113
119
  });
114
120
 
121
+ it('sanitizes raw HTML by default', () => {
122
+ const html = render(
123
+ '<div><img src="x.jpg" onerror="alert(1)"><script>alert(1)</script><span onclick="alert(1)">ok</span></div>',
124
+ );
125
+ expect(html).toContain('<img src="x.jpg" />');
126
+ expect(html).toContain('<span>ok</span>');
127
+ expect(html).not.toContain('<script>');
128
+ expect(html).not.toContain('onerror');
129
+ expect(html).not.toContain('onclick');
130
+ expect(html).not.toContain('javascript:');
131
+ });
132
+
133
+ it('strips raw HTML when requested', () => {
134
+ const html = render('<div><span>hidden</span></div>', { htmlPolicy: 'strip' });
135
+ expect(html).not.toContain('<span>hidden</span>');
136
+ expect(html).not.toContain('hidden');
137
+ });
138
+
139
+ it('preserves raw HTML only with the trusted opt-in', () => {
140
+ const html = render('<div><span onclick="alert(1)">ok</span></div>', {
141
+ htmlPolicy: 'trusted',
142
+ });
143
+ expect(html).toContain('<div><span onclick="alert(1)">ok</span></div>');
144
+ });
145
+
115
146
  it('escapes quotes in href and src attributes', () => {
116
147
  const html = render('![a](a"b.jpg)\n\n[c](d"e)');
117
148
  expect(html).toContain('&quot;');
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { MarkdownBlockNode, MarkdownDocument } from '@bendyline/squisq/markdown';
3
+ import type { Transition } from '@bendyline/squisq/schemas';
4
+ import { TRANSITION_TYPES } from '@bendyline/squisq/schemas';
5
+ import { markdownDocToPptx } from '../pptx/export';
6
+ import { openPackage, getPartXml } from '../ooxml/reader';
7
+ import { NS_PML, NS_PML_2010 } from '../ooxml/namespaces';
8
+
9
+ function docWithSecondSlideTransition(transition: Transition): MarkdownDocument {
10
+ return {
11
+ type: 'document',
12
+ children: [
13
+ {
14
+ type: 'heading',
15
+ depth: 2,
16
+ children: [{ type: 'text', value: 'One' }],
17
+ },
18
+ {
19
+ type: 'paragraph',
20
+ children: [{ type: 'text', value: 'Intro' }],
21
+ },
22
+ {
23
+ type: 'heading',
24
+ depth: 2,
25
+ children: [{ type: 'text', value: 'Two' }],
26
+ attributes: {
27
+ params: {
28
+ transition: transition.type,
29
+ ...(transition.duration !== undefined
30
+ ? { transitionDuration: String(transition.duration) }
31
+ : {}),
32
+ ...(transition.direction ? { transitionDirection: transition.direction } : {}),
33
+ },
34
+ blockMeta: { transition },
35
+ },
36
+ },
37
+ {
38
+ type: 'paragraph',
39
+ children: [{ type: 'text', value: 'Body' }],
40
+ },
41
+ ],
42
+ };
43
+ }
44
+
45
+ function docWithAllTransitions(): MarkdownDocument {
46
+ const children: MarkdownBlockNode[] = [
47
+ {
48
+ type: 'heading',
49
+ depth: 2,
50
+ children: [{ type: 'text', value: 'Transition Gallery' }],
51
+ },
52
+ {
53
+ type: 'paragraph',
54
+ children: [{ type: 'text', value: 'Intro slide before transition checks.' }],
55
+ },
56
+ ];
57
+
58
+ for (const type of TRANSITION_TYPES) {
59
+ const transition: Transition = { type, duration: 0.8 };
60
+ children.push(
61
+ {
62
+ type: 'heading',
63
+ depth: 2,
64
+ children: [{ type: 'text', value: type }],
65
+ attributes: {
66
+ params: { transition: type, transitionDuration: '0.8' },
67
+ blockMeta: { transition },
68
+ },
69
+ },
70
+ {
71
+ type: 'paragraph',
72
+ children: [{ type: 'text', value: `Transition token: ${type}` }],
73
+ },
74
+ );
75
+ }
76
+
77
+ return { type: 'document', children };
78
+ }
79
+
80
+ describe('markdownDocToPptx transitions', () => {
81
+ it('writes valid transition XML for every shared transition token', async () => {
82
+ const pkg = await openPackage(await markdownDocToPptx(docWithAllTransitions()));
83
+
84
+ for (let i = 0; i < TRANSITION_TYPES.length; i++) {
85
+ const type = TRANSITION_TYPES[i];
86
+ const slide = await getPartXml(pkg, `ppt/slides/slide${i + 2}.xml`);
87
+ expect(slide, type).not.toBeNull();
88
+
89
+ const transitions = slide!.getElementsByTagNameNS(NS_PML, 'transition');
90
+ if (type === 'cut') {
91
+ expect(transitions, type).toHaveLength(0);
92
+ continue;
93
+ }
94
+
95
+ expect(transitions, type).toHaveLength(1);
96
+ expect(transitions[0].getAttribute('spd'), type).toBe('med');
97
+
98
+ const childElements = Array.from(transitions[0].childNodes).filter(
99
+ (node): node is Element => node.nodeType === 1,
100
+ );
101
+ expect(childElements, type).toHaveLength(1);
102
+ expect([NS_PML, NS_PML_2010], type).toContain(childElements[0].namespaceURI);
103
+
104
+ if (childElements[0].namespaceURI === NS_PML_2010) {
105
+ expect(slide!.documentElement.getAttribute('mc:Ignorable'), type).toContain('p14');
106
+ }
107
+ }
108
+ });
109
+
110
+ it('writes standard PresentationML transition XML', async () => {
111
+ const md = docWithSecondSlideTransition({
112
+ type: 'checkerboard',
113
+ duration: 1.3,
114
+ direction: 'vertical',
115
+ });
116
+
117
+ const pkg = await openPackage(await markdownDocToPptx(md));
118
+ const slide = await getPartXml(pkg, 'ppt/slides/slide2.xml');
119
+ const transition = slide!.getElementsByTagNameNS(NS_PML, 'transition')[0];
120
+ const checker = transition.getElementsByTagNameNS(NS_PML, 'checker')[0];
121
+
122
+ expect(transition.getAttribute('spd')).toBe('slow');
123
+ expect(checker.getAttribute('dir')).toBe('vert');
124
+ });
125
+
126
+ it('writes PowerPoint 2010 transition XML when needed', async () => {
127
+ const md = docWithSecondSlideTransition({ type: 'flash', duration: 0.4 });
128
+
129
+ const pkg = await openPackage(await markdownDocToPptx(md));
130
+ const slide = await getPartXml(pkg, 'ppt/slides/slide2.xml');
131
+ const transition = slide!.getElementsByTagNameNS(NS_PML, 'transition')[0];
132
+ const flash = transition.getElementsByTagNameNS(NS_PML_2010, 'flash')[0];
133
+
134
+ expect(transition.getAttribute('spd')).toBe('fast');
135
+ expect(flash).toBeDefined();
136
+ expect(slide!.documentElement.getAttribute('mc:Ignorable')).toBe('p14');
137
+ });
138
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Tests for PPTX import: pptxToMarkdownDoc. Builds a minimal .pptx fixture with
3
+ * the shared OOXML writer, then imports it.
4
+ */
5
+
6
+ import { describe, expect, it } from 'vitest';
7
+ import type {
8
+ MarkdownHeading,
9
+ MarkdownList,
10
+ MarkdownParagraph,
11
+ MarkdownText,
12
+ } from '@bendyline/squisq/markdown';
13
+ import { NS_DRAWINGML, NS_PML, NS_R, REL_OFFICE_DOCUMENT, REL_SLIDE } from '../ooxml/namespaces';
14
+ import { createPackage } from '../ooxml/writer';
15
+ import { xmlDeclaration } from '../ooxml/xmlUtils';
16
+ import { pptxToMarkdownDoc } from '../pptx/import';
17
+
18
+ async function buildTestPptx(): Promise<ArrayBuffer> {
19
+ const pkg = createPackage();
20
+
21
+ pkg.addPart(
22
+ 'ppt/presentation.xml',
23
+ `${xmlDeclaration()}<p:presentation xmlns:p="${NS_PML}" xmlns:r="${NS_R}">` +
24
+ `<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`,
25
+ 'application/xml',
26
+ );
27
+ pkg.addPart(
28
+ 'ppt/slides/slide1.xml',
29
+ `${xmlDeclaration()}<p:sld xmlns:p="${NS_PML}" xmlns:a="${NS_DRAWINGML}"><p:cSld><p:spTree>` +
30
+ `<p:sp><p:nvSpPr><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>` +
31
+ `<p:txBody><a:p><a:r><a:t>My Title</a:t></a:r></a:p></p:txBody></p:sp>` +
32
+ `<p:sp><p:txBody>` +
33
+ `<a:p><a:r><a:t>First bullet</a:t></a:r></a:p>` +
34
+ `<a:p><a:r><a:t>Second bullet</a:t></a:r></a:p>` +
35
+ `</p:txBody></p:sp>` +
36
+ `</p:spTree></p:cSld></p:sld>`,
37
+ 'application/xml',
38
+ );
39
+
40
+ pkg.addRelationship('', {
41
+ id: 'rId1',
42
+ type: REL_OFFICE_DOCUMENT,
43
+ target: 'ppt/presentation.xml',
44
+ });
45
+ pkg.addRelationship('ppt/presentation.xml', {
46
+ id: 'rId1',
47
+ type: REL_SLIDE,
48
+ target: 'slides/slide1.xml',
49
+ });
50
+
51
+ return pkg.toArrayBuffer();
52
+ }
53
+
54
+ describe('pptxToMarkdownDoc', () => {
55
+ it('imports a slide as a heading + bullet list', async () => {
56
+ const doc = await pptxToMarkdownDoc(await buildTestPptx());
57
+ expect(doc.type).toBe('document');
58
+
59
+ const heading = doc.children[0] as MarkdownHeading;
60
+ expect(heading.type).toBe('heading');
61
+ expect(heading.depth).toBe(2);
62
+ expect((heading.children[0] as MarkdownText).value).toBe('My Title');
63
+
64
+ const list = doc.children[1] as MarkdownList;
65
+ expect(list.type).toBe('list');
66
+ expect(list.children).toHaveLength(2);
67
+ const firstItemPara = list.children[0]!.children[0] as MarkdownParagraph;
68
+ expect((firstItemPara.children[0] as MarkdownText).value).toBe('First bullet');
69
+ });
70
+ });