@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.
- package/dist/chunk-6M7Z25LA.js +46 -0
- package/dist/chunk-6M7Z25LA.js.map +1 -0
- package/dist/{chunk-S3Y7H2BK.js → chunk-APSIAEXL.js} +9 -8
- package/dist/chunk-APSIAEXL.js.map +1 -0
- package/dist/{chunk-UDS45KUJ.js → chunk-BJQIJ4S4.js} +36 -52
- package/dist/chunk-BJQIJ4S4.js.map +1 -0
- package/dist/{chunk-33YRFXZZ.js → chunk-CRTC6DEX.js} +259 -13
- package/dist/chunk-CRTC6DEX.js.map +1 -0
- package/dist/chunk-FMOIGA5T.js +201 -0
- package/dist/chunk-FMOIGA5T.js.map +1 -0
- package/dist/{chunk-VN2KEOYB.js → chunk-I2GFTEOV.js} +321 -91
- package/dist/chunk-I2GFTEOV.js.map +1 -0
- package/dist/chunk-JBNVE2GV.js +116 -0
- package/dist/chunk-JBNVE2GV.js.map +1 -0
- package/dist/chunk-L53YIGYS.js +141 -0
- package/dist/chunk-L53YIGYS.js.map +1 -0
- package/dist/{chunk-ERZ627GR.js → chunk-ON232HJR.js} +11 -26
- package/dist/chunk-ON232HJR.js.map +1 -0
- package/dist/chunk-PN52A5AA.js +32 -0
- package/dist/chunk-PN52A5AA.js.map +1 -0
- package/dist/{chunk-7DEUGIOO.js → chunk-QSE7EWE7.js} +12 -87
- package/dist/chunk-QSE7EWE7.js.map +1 -0
- package/dist/csv/index.d.ts +37 -0
- package/dist/csv/index.js +13 -0
- package/dist/csv/index.js.map +1 -0
- package/dist/docx/index.js +5 -3
- package/dist/epub/index.js +2 -1
- package/dist/html/index.d.ts +39 -2
- package/dist/html/index.js +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +31 -13
- package/dist/ooxml/index.js +16 -16
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.d.ts +20 -18
- package/dist/pptx/index.js +6 -2
- package/dist/xlsx/index.d.ts +20 -19
- package/dist/xlsx/index.js +2 -1
- package/package.json +7 -2
- package/src/__tests__/csvImport.test.ts +51 -0
- package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
- package/src/__tests__/htmlImport.test.ts +57 -0
- package/src/__tests__/plainHtml.test.ts +31 -0
- package/src/__tests__/pptxExport.test.ts +138 -0
- package/src/__tests__/pptxImport.test.ts +70 -0
- package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
- package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
- package/src/__tests__/roundTripMatrix.test.ts +136 -0
- package/src/__tests__/xlsxImport.test.ts +80 -0
- package/src/csv/index.ts +165 -0
- package/src/docx/export.ts +27 -44
- package/src/docx/import.ts +1 -2
- package/src/epub/export.ts +10 -27
- package/src/html/import.ts +297 -0
- package/src/html/index.ts +4 -0
- package/src/html/plainHtml.ts +66 -16
- package/src/index.ts +10 -3
- package/src/ooxml/namespaces.ts +3 -0
- package/src/pdf/export.ts +10 -5
- package/src/pdf/import.ts +1 -1
- package/src/pptx/export.ts +225 -85
- package/src/pptx/import.ts +156 -0
- package/src/pptx/index.ts +10 -28
- package/src/shared/inlineRuns.ts +99 -0
- package/src/shared/text.ts +41 -0
- package/src/xlsx/import.ts +159 -0
- package/src/xlsx/index.ts +11 -28
- package/dist/chunk-33YRFXZZ.js.map +0 -1
- package/dist/chunk-67KIJHV2.js +0 -21
- package/dist/chunk-67KIJHV2.js.map +0 -1
- package/dist/chunk-7DEUGIOO.js.map +0 -1
- package/dist/chunk-ERZ627GR.js.map +0 -1
- package/dist/chunk-S3Y7H2BK.js.map +0 -1
- package/dist/chunk-UDS45KUJ.js.map +0 -1
- package/dist/chunk-VN2KEOYB.js.map +0 -1
- package/dist/chunk-YN5HFCEW.js +0 -120
- package/dist/chunk-YN5HFCEW.js.map +0 -1
package/dist/pptx/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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(
|
|
115
|
+
declare function pptxToDoc(data: ArrayBuffer | Blob, options?: PptxImportOptions): Promise<Doc>;
|
|
114
116
|
|
|
115
117
|
export { type PptxExportOptions, type PptxImportOptions, docToPptx, markdownDocToPptx, pptxToDoc, pptxToMarkdownDoc };
|
package/dist/pptx/index.js
CHANGED
|
@@ -3,9 +3,13 @@ import {
|
|
|
3
3
|
markdownDocToPptx,
|
|
4
4
|
pptxToDoc,
|
|
5
5
|
pptxToMarkdownDoc
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
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,
|
package/dist/xlsx/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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(
|
|
57
|
+
declare function xlsxToDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<Doc>;
|
|
57
58
|
|
|
58
59
|
export { type XlsxExportOptions, type XlsxImportOptions, docToXlsx, markdownDocToXlsx, xlsxToDoc, xlsxToMarkdownDoc };
|
package/dist/xlsx/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-formats",
|
|
3
|
-
"version": "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.
|
|
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('\n\n[c](d"e)');
|
|
117
148
|
expect(html).toContain('"');
|
|
@@ -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
|
+
});
|