@bendyline/squisq-formats 1.3.0 → 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/container/index.d.ts +7 -5
- package/dist/container/index.js +39 -70
- package/dist/container/index.js.map +1 -1
- 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.d.ts +128 -15
- package/dist/docx/index.js +19 -22
- package/dist/docx/index.js.map +1 -1
- package/dist/epub/index.d.ts +66 -11
- package/dist/epub/index.js +11 -18
- package/dist/epub/index.js.map +1 -1
- package/dist/html/index.d.ts +315 -15
- package/dist/html/index.js +32 -165
- package/dist/html/index.js.map +1 -1
- package/dist/index.d.ts +10 -38
- package/dist/index.js +80 -37
- package/dist/index.js.map +1 -1
- package/dist/ooxml/index.d.ts +353 -14
- package/dist/ooxml/index.js +116 -22
- package/dist/ooxml/index.js.map +1 -1
- package/dist/pdf/index.d.ts +143 -17
- package/dist/pdf/index.js +16 -25
- package/dist/pdf/index.js.map +1 -1
- package/dist/pptx/index.d.ts +98 -21
- package/dist/pptx/index.js +18 -34
- package/dist/pptx/index.js.map +1 -1
- package/dist/xlsx/index.d.ts +29 -25
- package/dist/xlsx/index.js +13 -44
- package/dist/xlsx/index.js.map +1 -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/__tests__/container.test.d.ts +0 -8
- package/dist/__tests__/container.test.d.ts.map +0 -1
- package/dist/__tests__/container.test.js +0 -147
- package/dist/__tests__/container.test.js.map +0 -1
- package/dist/__tests__/docxExport.test.d.ts +0 -8
- package/dist/__tests__/docxExport.test.d.ts.map +0 -1
- package/dist/__tests__/docxExport.test.js +0 -383
- package/dist/__tests__/docxExport.test.js.map +0 -1
- package/dist/__tests__/docxImport.test.d.ts +0 -8
- package/dist/__tests__/docxImport.test.d.ts.map +0 -1
- package/dist/__tests__/docxImport.test.js +0 -309
- package/dist/__tests__/docxImport.test.js.map +0 -1
- package/dist/__tests__/epub.test.d.ts +0 -8
- package/dist/__tests__/epub.test.d.ts.map +0 -1
- package/dist/__tests__/epub.test.js +0 -472
- package/dist/__tests__/epub.test.js.map +0 -1
- package/dist/__tests__/html.test.d.ts +0 -8
- package/dist/__tests__/html.test.d.ts.map +0 -1
- package/dist/__tests__/html.test.js +0 -317
- package/dist/__tests__/html.test.js.map +0 -1
- package/dist/__tests__/ooxml.test.d.ts +0 -5
- package/dist/__tests__/ooxml.test.d.ts.map +0 -1
- package/dist/__tests__/ooxml.test.js +0 -152
- package/dist/__tests__/ooxml.test.js.map +0 -1
- package/dist/__tests__/pdfExport.test.d.ts +0 -8
- package/dist/__tests__/pdfExport.test.d.ts.map +0 -1
- package/dist/__tests__/pdfExport.test.js +0 -275
- package/dist/__tests__/pdfExport.test.js.map +0 -1
- package/dist/__tests__/pdfImport.test.d.ts +0 -11
- package/dist/__tests__/pdfImport.test.d.ts.map +0 -1
- package/dist/__tests__/pdfImport.test.js +0 -254
- package/dist/__tests__/pdfImport.test.js.map +0 -1
- package/dist/__tests__/plainHtml.test.d.ts +0 -5
- package/dist/__tests__/plainHtml.test.d.ts.map +0 -1
- package/dist/__tests__/plainHtml.test.js +0 -327
- package/dist/__tests__/plainHtml.test.js.map +0 -1
- package/dist/__tests__/plainHtmlBundle.test.d.ts +0 -11
- package/dist/__tests__/plainHtmlBundle.test.d.ts.map +0 -1
- package/dist/__tests__/plainHtmlBundle.test.js +0 -210
- package/dist/__tests__/plainHtmlBundle.test.js.map +0 -1
- package/dist/__tests__/roundTrip.test.d.ts +0 -7
- package/dist/__tests__/roundTrip.test.d.ts.map +0 -1
- package/dist/__tests__/roundTrip.test.js +0 -173
- package/dist/__tests__/roundTrip.test.js.map +0 -1
- 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/container/index.d.ts.map +0 -1
- package/dist/docx/export.d.ts +0 -69
- package/dist/docx/export.d.ts.map +0 -1
- package/dist/docx/export.js +0 -985
- package/dist/docx/export.js.map +0 -1
- package/dist/docx/import.d.ts +0 -65
- package/dist/docx/import.d.ts.map +0 -1
- package/dist/docx/import.js +0 -779
- package/dist/docx/import.js.map +0 -1
- package/dist/docx/index.d.ts.map +0 -1
- package/dist/docx/styles.d.ts +0 -68
- package/dist/docx/styles.d.ts.map +0 -1
- package/dist/docx/styles.js +0 -125
- package/dist/docx/styles.js.map +0 -1
- package/dist/epub/export.d.ts +0 -72
- package/dist/epub/export.d.ts.map +0 -1
- package/dist/epub/export.js +0 -698
- package/dist/epub/export.js.map +0 -1
- package/dist/epub/index.d.ts.map +0 -1
- package/dist/html/docsHtmlBundle.d.ts +0 -53
- package/dist/html/docsHtmlBundle.d.ts.map +0 -1
- package/dist/html/docsHtmlBundle.js +0 -299
- package/dist/html/docsHtmlBundle.js.map +0 -1
- package/dist/html/htmlTemplate.d.ts +0 -70
- package/dist/html/htmlTemplate.d.ts.map +0 -1
- package/dist/html/htmlTemplate.js +0 -282
- package/dist/html/htmlTemplate.js.map +0 -1
- package/dist/html/imageUtils.d.ts +0 -29
- package/dist/html/imageUtils.d.ts.map +0 -1
- package/dist/html/imageUtils.js +0 -63
- package/dist/html/imageUtils.js.map +0 -1
- package/dist/html/index.d.ts.map +0 -1
- package/dist/html/plainHtml.d.ts +0 -79
- package/dist/html/plainHtml.d.ts.map +0 -1
- package/dist/html/plainHtml.js +0 -614
- package/dist/html/plainHtml.js.map +0 -1
- package/dist/html/plainHtmlBundle.d.ts +0 -93
- package/dist/html/plainHtmlBundle.d.ts.map +0 -1
- package/dist/html/plainHtmlBundle.js +0 -357
- package/dist/html/plainHtmlBundle.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/ooxml/index.d.ts.map +0 -1
- package/dist/ooxml/namespaces.d.ts +0 -77
- package/dist/ooxml/namespaces.d.ts.map +0 -1
- package/dist/ooxml/namespaces.js +0 -104
- package/dist/ooxml/namespaces.js.map +0 -1
- package/dist/ooxml/reader.d.ts +0 -52
- package/dist/ooxml/reader.d.ts.map +0 -1
- package/dist/ooxml/reader.js +0 -190
- package/dist/ooxml/reader.js.map +0 -1
- package/dist/ooxml/types.d.ts +0 -82
- package/dist/ooxml/types.d.ts.map +0 -1
- package/dist/ooxml/types.js +0 -9
- package/dist/ooxml/types.js.map +0 -1
- package/dist/ooxml/writer.d.ts +0 -71
- package/dist/ooxml/writer.d.ts.map +0 -1
- package/dist/ooxml/writer.js +0 -200
- package/dist/ooxml/writer.js.map +0 -1
- package/dist/ooxml/xmlUtils.d.ts +0 -76
- package/dist/ooxml/xmlUtils.d.ts.map +0 -1
- package/dist/ooxml/xmlUtils.js +0 -108
- package/dist/ooxml/xmlUtils.js.map +0 -1
- package/dist/pdf/export.d.ts +0 -56
- package/dist/pdf/export.d.ts.map +0 -1
- package/dist/pdf/export.js +0 -768
- package/dist/pdf/export.js.map +0 -1
- package/dist/pdf/import.d.ts +0 -94
- package/dist/pdf/import.d.ts.map +0 -1
- package/dist/pdf/import.js +0 -796
- package/dist/pdf/import.js.map +0 -1
- package/dist/pdf/index.d.ts.map +0 -1
- package/dist/pdf/styles.d.ts +0 -142
- package/dist/pdf/styles.d.ts.map +0 -1
- package/dist/pdf/styles.js +0 -145
- package/dist/pdf/styles.js.map +0 -1
- package/dist/pptx/export.d.ts +0 -75
- package/dist/pptx/export.d.ts.map +0 -1
- package/dist/pptx/export.js +0 -724
- package/dist/pptx/export.js.map +0 -1
- package/dist/pptx/index.d.ts.map +0 -1
- package/dist/pptx/styles.d.ts +0 -54
- package/dist/pptx/styles.d.ts.map +0 -1
- package/dist/pptx/styles.js +0 -73
- package/dist/pptx/styles.js.map +0 -1
- package/dist/pptx/templates.d.ts +0 -24
- package/dist/pptx/templates.d.ts.map +0 -1
- package/dist/pptx/templates.js +0 -158
- package/dist/pptx/templates.js.map +0 -1
- package/dist/xlsx/index.d.ts.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import JSZip from 'jszip';
|
|
3
|
+
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
4
|
+
|
|
5
|
+
import { markdownDocToDocx } from '../docx/export';
|
|
6
|
+
import { docxToMarkdownDoc } from '../docx/import';
|
|
7
|
+
import { markdownDocToPptx } from '../pptx/export';
|
|
8
|
+
import { pptxToMarkdownDoc } from '../pptx/import';
|
|
9
|
+
import { markdownDocToPdf } from '../pdf/export';
|
|
10
|
+
import { pdfToMarkdownDoc } from '../pdf/import';
|
|
11
|
+
import { markdownDocToCsv, csvToMarkdownDoc } from '../csv/index';
|
|
12
|
+
import { markdownDocToEpub } from '../epub/export';
|
|
13
|
+
import { docToHtml } from '../html/index';
|
|
14
|
+
import { markdownDocToXlsx } from '../xlsx/index';
|
|
15
|
+
import { assertHybridRoundTrip, extractNormalizedText } from './roundTripMatrix.helpers';
|
|
16
|
+
import { ROUNDTRIP_FIXTURES } from './roundTripMatrix.fixtures';
|
|
17
|
+
|
|
18
|
+
const MOCK_PLAYER_SCRIPT = 'var SquisqPlayer={mount:function(){}};';
|
|
19
|
+
|
|
20
|
+
describe('format content-flow matrix', () => {
|
|
21
|
+
it('round-trips markdown through DOCX with hybrid equivalence', async () => {
|
|
22
|
+
const source = ROUNDTRIP_FIXTURES.story;
|
|
23
|
+
const docx = await markdownDocToDocx(source.doc);
|
|
24
|
+
const roundTrip = await docxToMarkdownDoc(docx);
|
|
25
|
+
|
|
26
|
+
assertHybridRoundTrip({
|
|
27
|
+
source: source.doc,
|
|
28
|
+
roundTrip,
|
|
29
|
+
keyPhrases: source.keyPhrases,
|
|
30
|
+
headingTexts: source.headingTexts,
|
|
31
|
+
requireHeadingOrder: true,
|
|
32
|
+
requireListSurvival: true,
|
|
33
|
+
minRetainRatio: 0.65,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('round-trips markdown through PPTX with hybrid equivalence', async () => {
|
|
38
|
+
const source = ROUNDTRIP_FIXTURES.mixed;
|
|
39
|
+
const pptx = await markdownDocToPptx(source.doc);
|
|
40
|
+
const roundTrip = await pptxToMarkdownDoc(pptx);
|
|
41
|
+
|
|
42
|
+
assertHybridRoundTrip({
|
|
43
|
+
source: source.doc,
|
|
44
|
+
roundTrip,
|
|
45
|
+
keyPhrases: source.keyPhrases,
|
|
46
|
+
headingTexts: source.headingTexts,
|
|
47
|
+
requireHeadingOrder: true,
|
|
48
|
+
minRetainRatio: 0.5,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('round-trips markdown through PDF with hybrid equivalence', async () => {
|
|
53
|
+
const source = ROUNDTRIP_FIXTURES.mixed;
|
|
54
|
+
const pdf = await markdownDocToPdf(source.doc, { title: 'Ops Weekly' });
|
|
55
|
+
const roundTrip = await pdfToMarkdownDoc(pdf, { bodyFontSize: 11 });
|
|
56
|
+
|
|
57
|
+
assertHybridRoundTrip({
|
|
58
|
+
source: source.doc,
|
|
59
|
+
roundTrip,
|
|
60
|
+
keyPhrases: ['ops weekly', 'launch readiness', 'rollback script', 'support tickets declined'],
|
|
61
|
+
minRetainRatio: 0.4,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('round-trips markdown through CSV for table-centric content', async () => {
|
|
66
|
+
const source = ROUNDTRIP_FIXTURES.table;
|
|
67
|
+
const csv = markdownDocToCsv(source.doc);
|
|
68
|
+
const roundTrip = await csvToMarkdownDoc(csv);
|
|
69
|
+
|
|
70
|
+
assertHybridRoundTrip({
|
|
71
|
+
source: source.doc,
|
|
72
|
+
roundTrip,
|
|
73
|
+
keyPhrases: ['throughput', '120 req/s', 'error rate', 'p95'],
|
|
74
|
+
requireTableSurvival: true,
|
|
75
|
+
minRetainRatio: 0.7,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('exports markdown to HTML as one-way smoke coverage', async () => {
|
|
80
|
+
const source = ROUNDTRIP_FIXTURES.story;
|
|
81
|
+
const doc = markdownToDoc(source.doc, { articleId: 'matrix-story' });
|
|
82
|
+
const html = docToHtml(doc, {
|
|
83
|
+
playerScript: MOCK_PLAYER_SCRIPT,
|
|
84
|
+
title: 'Matrix Story',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
88
|
+
const normalizedHtml = html.toLowerCase();
|
|
89
|
+
expect(normalizedHtml).toContain('river journal');
|
|
90
|
+
expect(normalizedHtml).toContain('north bank');
|
|
91
|
+
expect(normalizedHtml).toContain('full report');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('exports markdown to EPUB as one-way smoke coverage', async () => {
|
|
95
|
+
const source = ROUNDTRIP_FIXTURES.story;
|
|
96
|
+
const epub = await markdownDocToEpub(source.doc, {
|
|
97
|
+
title: 'River Journal',
|
|
98
|
+
author: 'Squisq Test',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const zip = await JSZip.loadAsync(epub);
|
|
102
|
+
expect(zip.file('OEBPS/content.opf')).toBeTruthy();
|
|
103
|
+
|
|
104
|
+
const chapter = await zip.file('OEBPS/chapters/chapter-001.xhtml')?.async('string');
|
|
105
|
+
expect(chapter).toBeTruthy();
|
|
106
|
+
|
|
107
|
+
const normalized = (chapter ?? '').toLowerCase();
|
|
108
|
+
expect(normalized).toContain('river journal');
|
|
109
|
+
expect(normalized).toContain('north bank');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('keeps high-value text through all bidirectional formats', async () => {
|
|
113
|
+
const source = ROUNDTRIP_FIXTURES.story;
|
|
114
|
+
|
|
115
|
+
const docxRoundTrip = await docxToMarkdownDoc(await markdownDocToDocx(source.doc));
|
|
116
|
+
const pptxRoundTrip = await pptxToMarkdownDoc(await markdownDocToPptx(source.doc));
|
|
117
|
+
const pdfRoundTrip = await pdfToMarkdownDoc(await markdownDocToPdf(source.doc));
|
|
118
|
+
const csvRoundTrip = await csvToMarkdownDoc(markdownDocToCsv(ROUNDTRIP_FIXTURES.table.doc));
|
|
119
|
+
|
|
120
|
+
const docxText = extractNormalizedText(docxRoundTrip);
|
|
121
|
+
const pptxText = extractNormalizedText(pptxRoundTrip);
|
|
122
|
+
const pdfText = extractNormalizedText(pdfRoundTrip);
|
|
123
|
+
const csvText = extractNormalizedText(csvRoundTrip);
|
|
124
|
+
|
|
125
|
+
expect(docxText).toContain('river journal');
|
|
126
|
+
expect(pptxText).toContain('river journal');
|
|
127
|
+
expect(pdfText).toContain('river journal');
|
|
128
|
+
expect(csvText).toContain('throughput');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('asserts current XLSX export stub behavior', async () => {
|
|
132
|
+
await expect(markdownDocToXlsx(ROUNDTRIP_FIXTURES.table.doc)).rejects.toThrow(
|
|
133
|
+
'XLSX export is not yet implemented',
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for XLSX import: xlsxToMarkdownDoc. Builds a minimal .xlsx fixture
|
|
3
|
+
* with the shared OOXML writer (dogfooding), then imports it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import type { MarkdownHeading, MarkdownTable, MarkdownText } from '@bendyline/squisq/markdown';
|
|
8
|
+
import { NS_R, NS_SML, REL_OFFICE_DOCUMENT } from '../ooxml/namespaces';
|
|
9
|
+
import { createPackage } from '../ooxml/writer';
|
|
10
|
+
import { xmlDeclaration } from '../ooxml/xmlUtils';
|
|
11
|
+
import { xlsxToMarkdownDoc } from '../xlsx/import';
|
|
12
|
+
|
|
13
|
+
const REL_WORKSHEET =
|
|
14
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
|
|
15
|
+
const REL_SHARED_STRINGS =
|
|
16
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
|
|
17
|
+
|
|
18
|
+
async function buildTestXlsx(): Promise<ArrayBuffer> {
|
|
19
|
+
const pkg = createPackage();
|
|
20
|
+
|
|
21
|
+
pkg.addPart(
|
|
22
|
+
'xl/workbook.xml',
|
|
23
|
+
`${xmlDeclaration()}<workbook xmlns="${NS_SML}" xmlns:r="${NS_R}">` +
|
|
24
|
+
`<sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets></workbook>`,
|
|
25
|
+
'application/xml',
|
|
26
|
+
);
|
|
27
|
+
pkg.addPart(
|
|
28
|
+
'xl/sharedStrings.xml',
|
|
29
|
+
`${xmlDeclaration()}<sst xmlns="${NS_SML}">` +
|
|
30
|
+
`<si><t>Name</t></si><si><t>Age</t></si><si><t>Alice</t></si></sst>`,
|
|
31
|
+
'application/xml',
|
|
32
|
+
);
|
|
33
|
+
pkg.addPart(
|
|
34
|
+
'xl/worksheets/sheet1.xml',
|
|
35
|
+
`${xmlDeclaration()}<worksheet xmlns="${NS_SML}"><sheetData>` +
|
|
36
|
+
`<row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c></row>` +
|
|
37
|
+
`<row r="2"><c r="A2" t="s"><v>2</v></c><c r="B2"><v>30</v></c></row>` +
|
|
38
|
+
`</sheetData></worksheet>`,
|
|
39
|
+
'application/xml',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
pkg.addRelationship('', { id: 'rId1', type: REL_OFFICE_DOCUMENT, target: 'xl/workbook.xml' });
|
|
43
|
+
pkg.addRelationship('xl/workbook.xml', {
|
|
44
|
+
id: 'rId1',
|
|
45
|
+
type: REL_WORKSHEET,
|
|
46
|
+
target: 'worksheets/sheet1.xml',
|
|
47
|
+
});
|
|
48
|
+
pkg.addRelationship('xl/workbook.xml', {
|
|
49
|
+
id: 'rId2',
|
|
50
|
+
type: REL_SHARED_STRINGS,
|
|
51
|
+
target: 'sharedStrings.xml',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return pkg.toArrayBuffer();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('xlsxToMarkdownDoc', () => {
|
|
58
|
+
it('imports a sheet as a heading + table, resolving shared strings', async () => {
|
|
59
|
+
const doc = await xlsxToMarkdownDoc(await buildTestXlsx());
|
|
60
|
+
expect(doc.type).toBe('document');
|
|
61
|
+
|
|
62
|
+
const heading = doc.children[0] as MarkdownHeading;
|
|
63
|
+
expect(heading.type).toBe('heading');
|
|
64
|
+
expect((heading.children[0] as MarkdownText).value).toBe('Data');
|
|
65
|
+
|
|
66
|
+
const table = doc.children[1] as MarkdownTable;
|
|
67
|
+
expect(table.type).toBe('table');
|
|
68
|
+
expect(table.children).toHaveLength(2);
|
|
69
|
+
const headerCell = table.children[0]!.children[0]!;
|
|
70
|
+
expect(headerCell.isHeader).toBe(true);
|
|
71
|
+
expect((headerCell.children[0] as MarkdownText).value).toBe('Name');
|
|
72
|
+
expect((table.children[1]!.children[0]!.children[0] as MarkdownText).value).toBe('Alice');
|
|
73
|
+
expect((table.children[1]!.children[1]!.children[0] as MarkdownText).value).toBe('30');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('selects a single sheet by name without a heading', async () => {
|
|
77
|
+
const doc = await xlsxToMarkdownDoc(await buildTestXlsx(), { sheet: 'Data' });
|
|
78
|
+
expect(doc.children[0]!.type).toBe('table');
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/csv/index.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bendyline/squisq-formats CSV Module
|
|
3
|
+
*
|
|
4
|
+
* Bridges CSV ↔ the squisq markdown table model. CSV isn't OOXML, so this
|
|
5
|
+
* module is self-contained (no jszip / DOMParser): a small RFC-4180 parser on
|
|
6
|
+
* the import side and a serializer on the export side. The first row is treated
|
|
7
|
+
* as the table header by default.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { csvToMarkdownDoc, markdownDocToCsv } from '@bendyline/squisq-formats/csv';
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
16
|
+
import type {
|
|
17
|
+
MarkdownDocument,
|
|
18
|
+
MarkdownTable,
|
|
19
|
+
MarkdownTableCell,
|
|
20
|
+
MarkdownTableRow,
|
|
21
|
+
} from '@bendyline/squisq/markdown';
|
|
22
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
23
|
+
|
|
24
|
+
export interface CsvImportOptions {
|
|
25
|
+
/** Field delimiter. Default `,`. */
|
|
26
|
+
delimiter?: string;
|
|
27
|
+
/** Treat the first row as a header row. Default true. */
|
|
28
|
+
hasHeader?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CsvExportOptions {
|
|
32
|
+
/** Field delimiter. Default `,`. */
|
|
33
|
+
delimiter?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function toText(data: ArrayBuffer | Blob | string): Promise<string> {
|
|
37
|
+
if (typeof data === 'string') return data;
|
|
38
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) return data.text();
|
|
39
|
+
return new TextDecoder().decode(new Uint8Array(data as ArrayBuffer));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse CSV text into a grid of string cells (RFC 4180: quotes, escaped quotes). */
|
|
43
|
+
export function parseCsv(text: string, delimiter = ','): string[][] {
|
|
44
|
+
const rows: string[][] = [];
|
|
45
|
+
let row: string[] = [];
|
|
46
|
+
let field = '';
|
|
47
|
+
let inQuotes = false;
|
|
48
|
+
let sawAny = false;
|
|
49
|
+
const pushField = () => {
|
|
50
|
+
row.push(field);
|
|
51
|
+
field = '';
|
|
52
|
+
};
|
|
53
|
+
const pushRow = () => {
|
|
54
|
+
pushField();
|
|
55
|
+
rows.push(row);
|
|
56
|
+
row = [];
|
|
57
|
+
};
|
|
58
|
+
for (let i = 0; i < text.length; i++) {
|
|
59
|
+
const ch = text[i]!;
|
|
60
|
+
if (inQuotes) {
|
|
61
|
+
if (ch === '"') {
|
|
62
|
+
if (text[i + 1] === '"') {
|
|
63
|
+
field += '"';
|
|
64
|
+
i++;
|
|
65
|
+
} else {
|
|
66
|
+
inQuotes = false;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
field += ch;
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (ch === '"') {
|
|
74
|
+
inQuotes = true;
|
|
75
|
+
sawAny = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === delimiter) {
|
|
79
|
+
sawAny = true;
|
|
80
|
+
pushField();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === '\n' || ch === '\r') {
|
|
84
|
+
if (ch === '\r' && text[i + 1] === '\n') i++;
|
|
85
|
+
pushRow();
|
|
86
|
+
sawAny = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
sawAny = true;
|
|
90
|
+
field += ch;
|
|
91
|
+
}
|
|
92
|
+
// Flush a trailing field/row only if the last line wasn't terminated.
|
|
93
|
+
if (field !== '' || row.length > 0 || sawAny) pushRow();
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rowsToTable(rows: string[][], hasHeader: boolean): MarkdownTable {
|
|
98
|
+
const maxCols = rows.reduce((m, r) => Math.max(m, r.length), 1);
|
|
99
|
+
const mdRows: MarkdownTableRow[] = rows.map((cells, rowIdx) => {
|
|
100
|
+
const children: MarkdownTableCell[] = [];
|
|
101
|
+
for (let c = 0; c < maxCols; c++) {
|
|
102
|
+
const value = cells[c] ?? '';
|
|
103
|
+
children.push({
|
|
104
|
+
type: 'tableCell',
|
|
105
|
+
...(hasHeader && rowIdx === 0 ? { isHeader: true } : {}),
|
|
106
|
+
children: value ? [{ type: 'text', value }] : [],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return { type: 'tableRow', children };
|
|
110
|
+
});
|
|
111
|
+
return { type: 'table', children: mdRows };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Convert CSV to a MarkdownDocument containing a single table. */
|
|
115
|
+
export async function csvToMarkdownDoc(
|
|
116
|
+
data: ArrayBuffer | Blob | string,
|
|
117
|
+
options: CsvImportOptions = {},
|
|
118
|
+
): Promise<MarkdownDocument> {
|
|
119
|
+
const text = await toText(data);
|
|
120
|
+
const rows = parseCsv(text, options.delimiter ?? ',');
|
|
121
|
+
const hasHeader = options.hasHeader ?? true;
|
|
122
|
+
const children = rows.length > 0 ? [rowsToTable(rows, hasHeader)] : [];
|
|
123
|
+
return { type: 'document', children };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Convert CSV to a squisq Doc. */
|
|
127
|
+
export async function csvToDoc(
|
|
128
|
+
data: ArrayBuffer | Blob | string,
|
|
129
|
+
options: CsvImportOptions = {},
|
|
130
|
+
): Promise<Doc> {
|
|
131
|
+
return markdownToDoc(await csvToMarkdownDoc(data, options));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function escapeCsvField(value: string, delimiter: string): string {
|
|
135
|
+
if (value.includes('"') || value.includes(delimiter) || /[\r\n]/.test(value)) {
|
|
136
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cellText(cell: MarkdownTableCell): string {
|
|
142
|
+
// Flatten inline children to plain text (CSV has no formatting).
|
|
143
|
+
const walk = (nodes: unknown[]): string =>
|
|
144
|
+
nodes
|
|
145
|
+
.map((n) => {
|
|
146
|
+
const node = n as { type?: string; value?: string; children?: unknown[] };
|
|
147
|
+
if (node.value !== undefined) return node.value;
|
|
148
|
+
if (Array.isArray(node.children)) return walk(node.children);
|
|
149
|
+
return '';
|
|
150
|
+
})
|
|
151
|
+
.join('');
|
|
152
|
+
return walk(cell.children);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Serialize the first table in a MarkdownDocument to CSV text. */
|
|
156
|
+
export function markdownDocToCsv(doc: MarkdownDocument, options: CsvExportOptions = {}): string {
|
|
157
|
+
const delimiter = options.delimiter ?? ',';
|
|
158
|
+
const table = doc.children.find((n) => n.type === 'table') as MarkdownTable | undefined;
|
|
159
|
+
if (!table) return '';
|
|
160
|
+
return table.children
|
|
161
|
+
.map((row) =>
|
|
162
|
+
row.children.map((cell) => escapeCsvField(cellText(cell), delimiter)).join(delimiter),
|
|
163
|
+
)
|
|
164
|
+
.join('\r\n');
|
|
165
|
+
}
|
package/src/docx/export.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import type { Doc, Theme } from '@bendyline/squisq/schemas';
|
|
21
|
-
import {
|
|
22
|
-
import { docToMarkdown } from '@bendyline/squisq/doc';
|
|
21
|
+
import { resolveFontFamily } from '@bendyline/squisq/schemas';
|
|
22
|
+
import { docToMarkdown, resolveThemeForDoc } from '@bendyline/squisq/doc';
|
|
23
23
|
import type {
|
|
24
24
|
MarkdownDocument,
|
|
25
25
|
MarkdownBlockNode,
|
|
@@ -44,6 +44,12 @@ import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
|
|
|
44
44
|
|
|
45
45
|
import { createPackage } from '../ooxml/writer.js';
|
|
46
46
|
import { xmlDeclaration, escapeXml } from '../ooxml/xmlUtils.js';
|
|
47
|
+
import { stripHtmlTags } from '../shared/text.js';
|
|
48
|
+
import {
|
|
49
|
+
inlineNodesToRuns,
|
|
50
|
+
inlineNodeToRuns,
|
|
51
|
+
type InlineRunHandlers,
|
|
52
|
+
} from '../shared/inlineRuns.js';
|
|
47
53
|
import {
|
|
48
54
|
NS_WML,
|
|
49
55
|
NS_R,
|
|
@@ -126,7 +132,7 @@ export async function markdownDocToDocx(
|
|
|
126
132
|
options.themeId !== undefined
|
|
127
133
|
? options
|
|
128
134
|
: { ...options, themeId: readFrontmatterThemeId(doc.frontmatter) };
|
|
129
|
-
const ctx = new ExportContext(resolvedOptions);
|
|
135
|
+
const ctx = new ExportContext(resolvedOptions, doc);
|
|
130
136
|
const bodyXml = convertBlocks(doc.children, ctx);
|
|
131
137
|
return buildDocxPackage(bodyXml, ctx, resolvedOptions);
|
|
132
138
|
}
|
|
@@ -206,7 +212,7 @@ class ExportContext {
|
|
|
206
212
|
|
|
207
213
|
private nextDocPrId = 1;
|
|
208
214
|
|
|
209
|
-
constructor(options: DocxExportOptions) {
|
|
215
|
+
constructor(options: DocxExportOptions, doc?: MarkdownDocument) {
|
|
210
216
|
let themeFont: string | undefined;
|
|
211
217
|
let themeTitleFont: string | undefined;
|
|
212
218
|
let themeHeadingColor: string | undefined;
|
|
@@ -215,7 +221,7 @@ class ExportContext {
|
|
|
215
221
|
let themeBackgroundColor: string | undefined;
|
|
216
222
|
|
|
217
223
|
if (options.themeId) {
|
|
218
|
-
const theme: Theme =
|
|
224
|
+
const theme: Theme = resolveThemeForDoc(doc, options.themeId);
|
|
219
225
|
// Theme fonts arrive as CSS stacks (e.g. `"Oswald", Impact,
|
|
220
226
|
// "Arial Black", sans-serif`). Word's `w:ascii` attribute is a
|
|
221
227
|
// single font name — passing the whole stack is treated as a
|
|
@@ -631,46 +637,30 @@ interface InlineFormat {
|
|
|
631
637
|
color?: string;
|
|
632
638
|
}
|
|
633
639
|
|
|
640
|
+
/**
|
|
641
|
+
* DOCX leaf handlers for the shared run-based inline walker. The traversal
|
|
642
|
+
* (format threading) lives in `shared/inlineRuns.ts`; these emit WordprocessingML.
|
|
643
|
+
*/
|
|
644
|
+
function docxRunHandlers(ctx: ExportContext): InlineRunHandlers {
|
|
645
|
+
return {
|
|
646
|
+
run: (text, format) => makeRun(text, format),
|
|
647
|
+
link: (node, format) => convertLink(node, ctx, format),
|
|
648
|
+
image: (node) => convertImage(node, ctx),
|
|
649
|
+
lineBreak: () => `<w:r><w:br/></w:r>`,
|
|
650
|
+
footnoteRef: (node) => convertFootnoteRef(node, ctx),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
634
654
|
function convertInlines(
|
|
635
655
|
nodes: MarkdownInlineNode[],
|
|
636
656
|
ctx: ExportContext,
|
|
637
657
|
format: InlineFormat = {},
|
|
638
658
|
): string {
|
|
639
|
-
|
|
640
|
-
for (const node of nodes) {
|
|
641
|
-
parts.push(convertInline(node, ctx, format));
|
|
642
|
-
}
|
|
643
|
-
return parts.join('');
|
|
659
|
+
return inlineNodesToRuns(nodes, docxRunHandlers(ctx), format);
|
|
644
660
|
}
|
|
645
661
|
|
|
646
662
|
function convertInline(node: MarkdownInlineNode, ctx: ExportContext, format: InlineFormat): string {
|
|
647
|
-
|
|
648
|
-
case 'text':
|
|
649
|
-
return makeRun(node.value, format);
|
|
650
|
-
case 'strong':
|
|
651
|
-
return convertInlines(node.children, ctx, { ...format, bold: true });
|
|
652
|
-
case 'emphasis':
|
|
653
|
-
return convertInlines(node.children, ctx, { ...format, italic: true });
|
|
654
|
-
case 'delete':
|
|
655
|
-
return convertInlines(node.children, ctx, { ...format, strike: true });
|
|
656
|
-
case 'inlineCode':
|
|
657
|
-
return makeRun(node.value, { ...format, code: true });
|
|
658
|
-
case 'link':
|
|
659
|
-
return convertLink(node, ctx, format);
|
|
660
|
-
case 'image':
|
|
661
|
-
return convertImage(node, ctx);
|
|
662
|
-
case 'break':
|
|
663
|
-
return `<w:r><w:br/></w:r>`;
|
|
664
|
-
case 'htmlInline':
|
|
665
|
-
return makeRun(stripHtmlTags(node.rawHtml), format);
|
|
666
|
-
case 'inlineMath':
|
|
667
|
-
return makeRun(node.value, { ...format, code: true });
|
|
668
|
-
case 'footnoteReference':
|
|
669
|
-
return convertFootnoteRef(node, ctx);
|
|
670
|
-
default:
|
|
671
|
-
// linkReference, imageReference, textDirective — skip or emit plain
|
|
672
|
-
return '';
|
|
673
|
-
}
|
|
663
|
+
return inlineNodeToRuns(node, docxRunHandlers(ctx), format);
|
|
674
664
|
}
|
|
675
665
|
|
|
676
666
|
function makeRun(text: string, format: InlineFormat): string {
|
|
@@ -1273,10 +1263,3 @@ function buildFootnotesXml(ctx: ExportContext): string {
|
|
|
1273
1263
|
// ============================================
|
|
1274
1264
|
// Helpers
|
|
1275
1265
|
// ============================================
|
|
1276
|
-
|
|
1277
|
-
/**
|
|
1278
|
-
* Strip HTML tags from a string, keeping only text content.
|
|
1279
|
-
*/
|
|
1280
|
-
function stripHtmlTags(html: string): string {
|
|
1281
|
-
return html.replace(/<[^>]*>/g, '');
|
|
1282
|
-
}
|
package/src/docx/import.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
23
23
|
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
24
|
+
import { stringifyMarkdown } from '@bendyline/squisq/markdown';
|
|
24
25
|
import type {
|
|
25
26
|
MarkdownDocument,
|
|
26
27
|
MarkdownBlockNode,
|
|
@@ -156,8 +157,6 @@ export async function docxToContainer(
|
|
|
156
157
|
const blocks = await convertBody(body, ctx);
|
|
157
158
|
const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
|
|
158
159
|
|
|
159
|
-
// Serialize to markdown
|
|
160
|
-
const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
161
160
|
const markdown = stringifyMarkdown(markdownDoc);
|
|
162
161
|
|
|
163
162
|
// Build container with markdown + images
|
package/src/epub/export.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
import JSZip from 'jszip';
|
|
25
25
|
import type { Doc, AudioSegment } from '@bendyline/squisq/schemas';
|
|
26
|
-
import {
|
|
26
|
+
import { resolveFontFamily } from '@bendyline/squisq/schemas';
|
|
27
|
+
import { resolveThemeForDoc } from '@bendyline/squisq/doc';
|
|
27
28
|
import type {
|
|
28
29
|
MarkdownDocument,
|
|
29
30
|
MarkdownBlockNode,
|
|
@@ -35,8 +36,10 @@ import type {
|
|
|
35
36
|
MarkdownTableRow,
|
|
36
37
|
MarkdownTableCell,
|
|
37
38
|
} from '@bendyline/squisq/markdown';
|
|
39
|
+
import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
|
|
38
40
|
import { escapeXml } from '../ooxml/xmlUtils.js';
|
|
39
41
|
import { inferMimeType, extractFilename } from '../html/imageUtils.js';
|
|
42
|
+
import { extractPlainText } from '../shared/text.js';
|
|
40
43
|
|
|
41
44
|
// ── Public API ────────────────────────────────────────────────────
|
|
42
45
|
|
|
@@ -119,8 +122,9 @@ export async function markdownDocToEpub(
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
// Generate theme CSS
|
|
123
|
-
|
|
125
|
+
// Generate theme CSS — honor an explicit themeId, else the doc's frontmatter
|
|
126
|
+
// theme (`squisq-theme` / legacy), mirroring the other export formats.
|
|
127
|
+
const css = generateStylesheet(options.themeId ?? readFrontmatterThemeId(doc.frontmatter), doc);
|
|
124
128
|
|
|
125
129
|
// Build the ZIP
|
|
126
130
|
const zip = new JSZip();
|
|
@@ -360,28 +364,7 @@ function splitIntoChapters(nodes: MarkdownBlockNode[]): Chapter[] {
|
|
|
360
364
|
}
|
|
361
365
|
|
|
362
366
|
function extractHeadingText(heading: MarkdownHeading): string {
|
|
363
|
-
return heading.children
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function inlineToText(node: MarkdownInlineNode): string {
|
|
367
|
-
switch (node.type) {
|
|
368
|
-
case 'text':
|
|
369
|
-
return node.value;
|
|
370
|
-
case 'emphasis':
|
|
371
|
-
case 'strong':
|
|
372
|
-
case 'delete':
|
|
373
|
-
return node.children.map(inlineToText).join('');
|
|
374
|
-
case 'inlineCode':
|
|
375
|
-
return node.value;
|
|
376
|
-
case 'link':
|
|
377
|
-
return node.children.map(inlineToText).join('');
|
|
378
|
-
case 'image':
|
|
379
|
-
return node.alt ?? '';
|
|
380
|
-
case 'break':
|
|
381
|
-
return ' ';
|
|
382
|
-
default:
|
|
383
|
-
return '';
|
|
384
|
-
}
|
|
367
|
+
return extractPlainText(heading.children);
|
|
385
368
|
}
|
|
386
369
|
|
|
387
370
|
// ── Image Collection ──────────────────────────────────────────────
|
|
@@ -795,10 +778,10 @@ ${navItems}
|
|
|
795
778
|
|
|
796
779
|
// ── Stylesheet ────────────────────────────────────────────────────
|
|
797
780
|
|
|
798
|
-
function generateStylesheet(themeId?:
|
|
781
|
+
function generateStylesheet(themeId: string | undefined, doc?: MarkdownDocument): string {
|
|
799
782
|
let themeVars = '';
|
|
800
783
|
if (themeId) {
|
|
801
|
-
const theme =
|
|
784
|
+
const theme = resolveThemeForDoc(doc, themeId);
|
|
802
785
|
themeVars = `
|
|
803
786
|
--epub-bg: ${theme.colors.background};
|
|
804
787
|
--epub-text: ${theme.colors.text};
|