@bendyline/squisq-formats 2.1.0 → 2.2.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/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +1 -1
- package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
- package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
- package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
- package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
- package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
- package/dist/chunk-GX7RAUME.js +121 -0
- package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
- package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
- package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
- package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
- package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
- package/dist/chunk-OGS5VCGJ.js +446 -0
- package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
- package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
- package/dist/container/index.js +1 -2
- package/dist/csv/index.d.ts +27 -2
- package/dist/csv/index.js +1 -2
- package/dist/docx/index.d.ts +5 -1
- package/dist/docx/index.js +9 -11
- package/dist/epub/index.d.ts +2 -0
- package/dist/epub/index.js +5 -6
- package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
- package/dist/extract-MN7LA3NL.js +13 -0
- package/dist/html/index.d.ts +11 -4
- package/dist/html/index.js +3 -4
- package/dist/images-ESPQKVTW.js +6 -0
- package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
- package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +28 -26
- package/dist/infer/index.d.ts +3 -3
- package/dist/infer/index.js +7 -9
- package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
- package/dist/layouts-DRWZGSPD.js +10 -0
- package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
- package/dist/ooxml/index.d.ts +3 -3
- package/dist/ooxml/index.js +14 -13
- package/dist/pdf/index.d.ts +18 -0
- package/dist/pdf/index.js +2 -3
- package/dist/pptx/index.d.ts +4 -4
- package/dist/pptx/index.js +11 -13
- package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
- package/dist/registry/index.d.ts +21 -5
- package/dist/registry/index.js +9 -6
- package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
- package/dist/xlsx/index.d.ts +3 -3
- package/dist/xlsx/index.js +6 -7
- package/package.json +6 -3
- package/dist/chunk-4VUWTSGM.js.map +0 -1
- package/dist/chunk-6M7Z25LA.js +0 -46
- package/dist/chunk-6M7Z25LA.js.map +0 -1
- package/dist/chunk-74GO3FVS.js.map +0 -1
- package/dist/chunk-7ARKUCQT.js.map +0 -1
- package/dist/chunk-DTDF6QDP.js.map +0 -1
- package/dist/chunk-EW54IRRS.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-KURGXM4I.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.js.map +0 -1
- package/dist/chunk-NNHKUXKA.js.map +0 -1
- package/dist/chunk-O3GVVND4.js.map +0 -1
- package/dist/chunk-ODL3SSPT.js.map +0 -1
- package/dist/chunk-PN52A5AA.js.map +0 -1
- package/dist/chunk-QFLDYKCR.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-RLU7UFYU.js.map +0 -1
- package/dist/chunk-SC67HYQJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-UGYF5AZE.js +0 -275
- package/dist/chunk-UGYF5AZE.js.map +0 -1
- package/dist/chunk-WQSHGBLN.js.map +0 -1
- package/dist/chunk-YRT7GQ5Y.js +0 -28
- package/dist/chunk-YRT7GQ5Y.js.map +0 -1
- package/dist/container/index.js.map +0 -1
- package/dist/csv/index.js.map +0 -1
- package/dist/docx/index.js.map +0 -1
- package/dist/epub/index.js.map +0 -1
- package/dist/extract-OJ7ZQV6P.js +0 -15
- package/dist/extract-OJ7ZQV6P.js.map +0 -1
- package/dist/html/index.js.map +0 -1
- package/dist/images-7FBWPKE3.js +0 -7
- package/dist/images-7FBWPKE3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infer/index.js.map +0 -1
- package/dist/layouts-5VDIRPIJ.js +0 -12
- package/dist/layouts-5VDIRPIJ.js.map +0 -1
- package/dist/mapTheme-IR27S6IV.js.map +0 -1
- package/dist/ooxml/index.js.map +0 -1
- package/dist/pdf/index.js.map +0 -1
- package/dist/pptx/index.js.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/xlsx/index.js.map +0 -1
- package/src/__tests__/container.test.ts +0 -230
- package/src/__tests__/convert.test.ts +0 -495
- package/src/__tests__/csvImport.test.ts +0 -84
- package/src/__tests__/docxExport.test.ts +0 -491
- package/src/__tests__/docxImport.test.ts +0 -531
- package/src/__tests__/epub.test.ts +0 -649
- package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
- package/src/__tests__/formatRegistry.test.ts +0 -174
- package/src/__tests__/html.test.ts +0 -439
- package/src/__tests__/htmlImport.test.ts +0 -57
- package/src/__tests__/inferTheme.test.ts +0 -135
- package/src/__tests__/lossyWarnings.test.ts +0 -146
- package/src/__tests__/ooxml.test.ts +0 -271
- package/src/__tests__/ooxmlCancellation.test.ts +0 -113
- package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
- package/src/__tests__/pdfExport.test.ts +0 -322
- package/src/__tests__/pdfImport.test.ts +0 -384
- package/src/__tests__/plainHtml.test.ts +0 -417
- package/src/__tests__/plainHtmlBundle.test.ts +0 -253
- package/src/__tests__/pptxExport.test.ts +0 -138
- package/src/__tests__/pptxImport.test.ts +0 -145
- package/src/__tests__/pptxInferFixtures.ts +0 -314
- package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
- package/src/__tests__/roundTrip.test.ts +0 -201
- package/src/__tests__/roundTripAssets.test.ts +0 -50
- package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
- package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
- package/src/__tests__/roundTripMatrix.test.ts +0 -142
- package/src/__tests__/sharedContainer.test.ts +0 -41
- package/src/__tests__/sharedImages.test.ts +0 -61
- package/src/__tests__/xlsxExport.test.ts +0 -164
- package/src/__tests__/xlsxImport.test.ts +0 -80
- package/src/__tests__/zipSafety.test.ts +0 -317
- package/src/container/index.ts +0 -94
- package/src/csv/index.ts +0 -188
- package/src/docx/export.ts +0 -1375
- package/src/docx/import.ts +0 -1250
- package/src/docx/index.ts +0 -26
- package/src/docx/styles.ts +0 -145
- package/src/epub/export.ts +0 -968
- package/src/epub/index.ts +0 -20
- package/src/html/docsHtmlBundle.ts +0 -373
- package/src/html/htmlTemplate.ts +0 -385
- package/src/html/imageUtils.ts +0 -61
- package/src/html/import.ts +0 -297
- package/src/html/index.ts +0 -212
- package/src/html/plainHtml.ts +0 -790
- package/src/html/plainHtmlBundle.ts +0 -421
- package/src/index.ts +0 -109
- package/src/infer/extract.ts +0 -127
- package/src/infer/index.ts +0 -199
- package/src/infer/mapTheme.ts +0 -176
- package/src/infer/types.ts +0 -27
- package/src/ooxml/index.ts +0 -111
- package/src/ooxml/namespaces.ts +0 -217
- package/src/ooxml/readUtils.ts +0 -44
- package/src/ooxml/reader.ts +0 -318
- package/src/ooxml/themeReader.ts +0 -197
- package/src/ooxml/types.ts +0 -103
- package/src/ooxml/writer.ts +0 -339
- package/src/ooxml/xmlUtils.ts +0 -123
- package/src/pdf/export.ts +0 -1084
- package/src/pdf/import.ts +0 -1164
- package/src/pdf/index.ts +0 -29
- package/src/pdf/styles.ts +0 -180
- package/src/pptx/export.ts +0 -1184
- package/src/pptx/import.ts +0 -455
- package/src/pptx/index.ts +0 -52
- package/src/pptx/layouts.ts +0 -1222
- package/src/pptx/styles.ts +0 -96
- package/src/pptx/templates.ts +0 -187
- package/src/registry/convert.ts +0 -433
- package/src/registry/defaultFormats.ts +0 -413
- package/src/registry/errors.ts +0 -46
- package/src/registry/index.ts +0 -43
- package/src/registry/registry.ts +0 -48
- package/src/registry/types.ts +0 -170
- package/src/shared/boundedZipArchive.ts +0 -383
- package/src/shared/container.ts +0 -28
- package/src/shared/fidelity.ts +0 -130
- package/src/shared/images.ts +0 -44
- package/src/shared/inlineRuns.ts +0 -99
- package/src/shared/text.ts +0 -41
- package/src/shared/zipEntryCount.ts +0 -151
- package/src/shared/zipLimits.ts +0 -296
- package/src/shared/zipSafety.ts +0 -19
- package/src/xlsx/export.ts +0 -253
- package/src/xlsx/import.ts +0 -160
- package/src/xlsx/index.ts +0 -35
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown, type MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
2
|
-
|
|
3
|
-
export interface RoundTripFixture {
|
|
4
|
-
name: string;
|
|
5
|
-
markdown: string;
|
|
6
|
-
doc: MarkdownDocument;
|
|
7
|
-
keyPhrases: string[];
|
|
8
|
-
headingTexts: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function makeFixture(
|
|
12
|
-
name: string,
|
|
13
|
-
markdown: string,
|
|
14
|
-
keyPhrases: string[],
|
|
15
|
-
headingTexts: string[],
|
|
16
|
-
): RoundTripFixture {
|
|
17
|
-
return {
|
|
18
|
-
name,
|
|
19
|
-
markdown,
|
|
20
|
-
doc: parseMarkdown(markdown),
|
|
21
|
-
keyPhrases,
|
|
22
|
-
headingTexts,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const ROUNDTRIP_FIXTURES = {
|
|
27
|
-
story: makeFixture(
|
|
28
|
-
'story',
|
|
29
|
-
`# River Journal
|
|
30
|
-
|
|
31
|
-
The field team walked the north bank and recorded water conditions.
|
|
32
|
-
|
|
33
|
-
## Morning Snapshot
|
|
34
|
-
|
|
35
|
-
- Water level is steady.
|
|
36
|
-
- Team status is ready.
|
|
37
|
-
- Safety checks are complete.
|
|
38
|
-
|
|
39
|
-
## Notes
|
|
40
|
-
|
|
41
|
-
The crew published an update and linked the [full report](https://example.com/report).
|
|
42
|
-
`,
|
|
43
|
-
['river journal', 'north bank', 'water level', 'team status', 'full report'],
|
|
44
|
-
['River Journal', 'Morning Snapshot', 'Notes'],
|
|
45
|
-
),
|
|
46
|
-
|
|
47
|
-
mixed: makeFixture(
|
|
48
|
-
'mixed',
|
|
49
|
-
`# Ops Weekly
|
|
50
|
-
|
|
51
|
-
This week we tracked **launch readiness**, *quality signals*, and risk burn-down.
|
|
52
|
-
|
|
53
|
-
## Checklist
|
|
54
|
-
|
|
55
|
-
1. Confirm deployment window.
|
|
56
|
-
2. Validate rollback script.
|
|
57
|
-
3. Notify stakeholders.
|
|
58
|
-
|
|
59
|
-
## Outcome
|
|
60
|
-
|
|
61
|
-
Launch readiness improved and support tickets declined.
|
|
62
|
-
`,
|
|
63
|
-
[
|
|
64
|
-
'ops weekly',
|
|
65
|
-
'launch readiness',
|
|
66
|
-
'quality signals',
|
|
67
|
-
'rollback script',
|
|
68
|
-
'support tickets declined',
|
|
69
|
-
],
|
|
70
|
-
['Ops Weekly', 'Checklist', 'Outcome'],
|
|
71
|
-
),
|
|
72
|
-
|
|
73
|
-
table: makeFixture(
|
|
74
|
-
'table',
|
|
75
|
-
`# Metrics
|
|
76
|
-
|
|
77
|
-
| Metric | Value |
|
|
78
|
-
| --- | --- |
|
|
79
|
-
| Throughput | 120 req/s |
|
|
80
|
-
| Error Rate | 0.2% |
|
|
81
|
-
| P95 | 180ms |
|
|
82
|
-
`,
|
|
83
|
-
['metrics', 'throughput', '120 req/s', 'error rate', 'p95'],
|
|
84
|
-
['Metrics'],
|
|
85
|
-
),
|
|
86
|
-
};
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { expect } from 'vitest';
|
|
2
|
-
import type {
|
|
3
|
-
MarkdownDocument,
|
|
4
|
-
MarkdownNode,
|
|
5
|
-
MarkdownBlockNode,
|
|
6
|
-
MarkdownHeading,
|
|
7
|
-
} from '@bendyline/squisq/markdown';
|
|
8
|
-
|
|
9
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
|
-
return typeof value === 'object' && value !== null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function collectText(node: unknown, out: string[]): void {
|
|
14
|
-
if (!isRecord(node)) return;
|
|
15
|
-
|
|
16
|
-
const maybeType = node.type;
|
|
17
|
-
if (typeof maybeType === 'string') {
|
|
18
|
-
if (maybeType === 'text' && typeof node.value === 'string') {
|
|
19
|
-
out.push(node.value);
|
|
20
|
-
}
|
|
21
|
-
if ((maybeType === 'code' || maybeType === 'inlineCode') && typeof node.value === 'string') {
|
|
22
|
-
out.push(node.value);
|
|
23
|
-
}
|
|
24
|
-
if (maybeType === 'link' && typeof node.url === 'string') {
|
|
25
|
-
out.push(node.url);
|
|
26
|
-
}
|
|
27
|
-
if (maybeType === 'image') {
|
|
28
|
-
if (typeof node.alt === 'string') out.push(node.alt);
|
|
29
|
-
if (typeof node.url === 'string') out.push(node.url);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const children = node.children;
|
|
34
|
-
if (Array.isArray(children)) {
|
|
35
|
-
for (const child of children) collectText(child, out);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeText(input: string): string {
|
|
40
|
-
return input
|
|
41
|
-
.toLowerCase()
|
|
42
|
-
.replace(/\r\n/g, '\n')
|
|
43
|
-
.replace(/[^a-z0-9%/.:\-\s]+/g, ' ')
|
|
44
|
-
.replace(/\s+/g, ' ')
|
|
45
|
-
.trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function compactText(input: string): string {
|
|
49
|
-
return normalizeText(input).replace(/\s+/g, '');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function extractDocumentText(doc: MarkdownDocument): string {
|
|
53
|
-
const out: string[] = [];
|
|
54
|
-
collectText(doc, out);
|
|
55
|
-
return normalizeText(out.join(' '));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function extractHeadingTexts(doc: MarkdownDocument): string[] {
|
|
59
|
-
return doc.children
|
|
60
|
-
.filter((block): block is MarkdownHeading => block.type === 'heading')
|
|
61
|
-
.map((heading) => {
|
|
62
|
-
const out: string[] = [];
|
|
63
|
-
collectText(heading, out);
|
|
64
|
-
return normalizeText(out.join(' '));
|
|
65
|
-
})
|
|
66
|
-
.filter(Boolean);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function containsNodeType(node: unknown, wantedType: string): boolean {
|
|
70
|
-
if (!isRecord(node)) return false;
|
|
71
|
-
if (node.type === wantedType) return true;
|
|
72
|
-
const children = node.children;
|
|
73
|
-
if (!Array.isArray(children)) return false;
|
|
74
|
-
return children.some((child) => containsNodeType(child, wantedType));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function retainRatio(source: string, roundTrip: string): number {
|
|
78
|
-
const sourceTokens = source.split(' ').filter((t) => t.length >= 4);
|
|
79
|
-
if (sourceTokens.length === 0) return 1;
|
|
80
|
-
|
|
81
|
-
const targetSet = new Set(roundTrip.split(' ').filter((t) => t.length >= 4));
|
|
82
|
-
let matched = 0;
|
|
83
|
-
for (const token of sourceTokens) {
|
|
84
|
-
if (targetSet.has(token)) matched++;
|
|
85
|
-
}
|
|
86
|
-
return matched / sourceTokens.length;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface HybridRoundTripAssertions {
|
|
90
|
-
source: MarkdownDocument;
|
|
91
|
-
roundTrip: MarkdownDocument;
|
|
92
|
-
keyPhrases: string[];
|
|
93
|
-
headingTexts?: string[];
|
|
94
|
-
requireHeadingOrder?: boolean;
|
|
95
|
-
requireListSurvival?: boolean;
|
|
96
|
-
requireTableSurvival?: boolean;
|
|
97
|
-
minRetainRatio?: number;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function assertHybridRoundTrip(args: HybridRoundTripAssertions): void {
|
|
101
|
-
const sourceText = extractDocumentText(args.source);
|
|
102
|
-
const roundTripText = extractDocumentText(args.roundTrip);
|
|
103
|
-
|
|
104
|
-
expect(roundTripText.length).toBeGreaterThan(0);
|
|
105
|
-
|
|
106
|
-
const minRetainRatio = args.minRetainRatio ?? 0.5;
|
|
107
|
-
expect(retainRatio(sourceText, roundTripText)).toBeGreaterThanOrEqual(minRetainRatio);
|
|
108
|
-
|
|
109
|
-
for (const phrase of args.keyPhrases) {
|
|
110
|
-
const normalizedPhrase = normalizeText(phrase);
|
|
111
|
-
if (roundTripText.includes(normalizedPhrase)) continue;
|
|
112
|
-
expect(compactText(roundTripText)).toContain(compactText(normalizedPhrase));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (args.requireHeadingOrder && args.headingTexts && args.headingTexts.length > 0) {
|
|
116
|
-
const normalizedExpected = args.headingTexts.map((h) => normalizeText(h));
|
|
117
|
-
const actualHeadings = extractHeadingTexts(args.roundTrip);
|
|
118
|
-
|
|
119
|
-
let cursor = -1;
|
|
120
|
-
for (const expectedHeading of normalizedExpected) {
|
|
121
|
-
const next = actualHeadings.findIndex(
|
|
122
|
-
(heading, index) => index > cursor && heading.includes(expectedHeading),
|
|
123
|
-
);
|
|
124
|
-
expect(next).toBeGreaterThan(cursor);
|
|
125
|
-
cursor = next;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (args.requireListSurvival && containsNodeType(args.source, 'list')) {
|
|
130
|
-
expect(containsNodeType(args.roundTrip, 'list')).toBe(true);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (args.requireTableSurvival && containsNodeType(args.source, 'table')) {
|
|
134
|
-
expect(containsNodeType(args.roundTrip, 'table')).toBe(true);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function extractNormalizedText(doc: MarkdownDocument): string {
|
|
139
|
-
return extractDocumentText(doc);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function docHasNodeType(doc: MarkdownDocument, wantedType: MarkdownNode['type']): boolean {
|
|
143
|
-
return containsNodeType(doc, wantedType);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function firstHeadingText(doc: MarkdownDocument): string | null {
|
|
147
|
-
const firstHeading = doc.children.find(
|
|
148
|
-
(block): block is MarkdownBlockNode & { type: 'heading' } => block.type === 'heading',
|
|
149
|
-
);
|
|
150
|
-
if (!firstHeading) return null;
|
|
151
|
-
const out: string[] = [];
|
|
152
|
-
collectText(firstHeading, out);
|
|
153
|
-
return normalizeText(out.join(' '));
|
|
154
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
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 { xlsxToMarkdownDoc } from '../xlsx/import';
|
|
16
|
-
import { assertHybridRoundTrip, extractNormalizedText } from './roundTripMatrix.helpers';
|
|
17
|
-
import { ROUNDTRIP_FIXTURES } from './roundTripMatrix.fixtures';
|
|
18
|
-
|
|
19
|
-
const MOCK_PLAYER_SCRIPT = 'var SquisqPlayer={mount:function(){}};';
|
|
20
|
-
|
|
21
|
-
describe('format content-flow matrix', () => {
|
|
22
|
-
it('round-trips markdown through DOCX with hybrid equivalence', async () => {
|
|
23
|
-
const source = ROUNDTRIP_FIXTURES.story;
|
|
24
|
-
const docx = await markdownDocToDocx(source.doc);
|
|
25
|
-
const roundTrip = await docxToMarkdownDoc(docx);
|
|
26
|
-
|
|
27
|
-
assertHybridRoundTrip({
|
|
28
|
-
source: source.doc,
|
|
29
|
-
roundTrip,
|
|
30
|
-
keyPhrases: source.keyPhrases,
|
|
31
|
-
headingTexts: source.headingTexts,
|
|
32
|
-
requireHeadingOrder: true,
|
|
33
|
-
requireListSurvival: true,
|
|
34
|
-
minRetainRatio: 0.65,
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('round-trips markdown through PPTX with hybrid equivalence', async () => {
|
|
39
|
-
const source = ROUNDTRIP_FIXTURES.mixed;
|
|
40
|
-
const pptx = await markdownDocToPptx(source.doc);
|
|
41
|
-
const roundTrip = await pptxToMarkdownDoc(pptx);
|
|
42
|
-
|
|
43
|
-
assertHybridRoundTrip({
|
|
44
|
-
source: source.doc,
|
|
45
|
-
roundTrip,
|
|
46
|
-
keyPhrases: source.keyPhrases,
|
|
47
|
-
headingTexts: source.headingTexts,
|
|
48
|
-
requireHeadingOrder: true,
|
|
49
|
-
minRetainRatio: 0.5,
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('round-trips markdown through PDF with hybrid equivalence', async () => {
|
|
54
|
-
const source = ROUNDTRIP_FIXTURES.mixed;
|
|
55
|
-
const pdf = await markdownDocToPdf(source.doc, { title: 'Ops Weekly' });
|
|
56
|
-
const roundTrip = await pdfToMarkdownDoc(pdf, { bodyFontSize: 11 });
|
|
57
|
-
|
|
58
|
-
assertHybridRoundTrip({
|
|
59
|
-
source: source.doc,
|
|
60
|
-
roundTrip,
|
|
61
|
-
keyPhrases: ['ops weekly', 'launch readiness', 'rollback script', 'support tickets declined'],
|
|
62
|
-
minRetainRatio: 0.4,
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('round-trips markdown through CSV for table-centric content', async () => {
|
|
67
|
-
const source = ROUNDTRIP_FIXTURES.table;
|
|
68
|
-
const csv = markdownDocToCsv(source.doc);
|
|
69
|
-
const roundTrip = await csvToMarkdownDoc(csv);
|
|
70
|
-
|
|
71
|
-
assertHybridRoundTrip({
|
|
72
|
-
source: source.doc,
|
|
73
|
-
roundTrip,
|
|
74
|
-
keyPhrases: ['throughput', '120 req/s', 'error rate', 'p95'],
|
|
75
|
-
requireTableSurvival: true,
|
|
76
|
-
minRetainRatio: 0.7,
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('exports markdown to HTML as one-way smoke coverage', async () => {
|
|
81
|
-
const source = ROUNDTRIP_FIXTURES.story;
|
|
82
|
-
const doc = markdownToDoc(source.doc, { articleId: 'matrix-story' });
|
|
83
|
-
const html = docToHtml(doc, {
|
|
84
|
-
playerScript: MOCK_PLAYER_SCRIPT,
|
|
85
|
-
title: 'Matrix Story',
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(html).toContain('<!DOCTYPE html>');
|
|
89
|
-
const normalizedHtml = html.toLowerCase();
|
|
90
|
-
expect(normalizedHtml).toContain('river journal');
|
|
91
|
-
expect(normalizedHtml).toContain('north bank');
|
|
92
|
-
expect(normalizedHtml).toContain('full report');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('exports markdown to EPUB as one-way smoke coverage', async () => {
|
|
96
|
-
const source = ROUNDTRIP_FIXTURES.story;
|
|
97
|
-
const epub = await markdownDocToEpub(source.doc, {
|
|
98
|
-
title: 'River Journal',
|
|
99
|
-
author: 'Squisq Test',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const zip = await JSZip.loadAsync(epub);
|
|
103
|
-
expect(zip.file('OEBPS/content.opf')).toBeTruthy();
|
|
104
|
-
|
|
105
|
-
const chapter = await zip.file('OEBPS/chapters/chapter-001.xhtml')?.async('string');
|
|
106
|
-
expect(chapter).toBeTruthy();
|
|
107
|
-
|
|
108
|
-
const normalized = (chapter ?? '').toLowerCase();
|
|
109
|
-
expect(normalized).toContain('river journal');
|
|
110
|
-
expect(normalized).toContain('north bank');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('keeps high-value text through all bidirectional formats', async () => {
|
|
114
|
-
const source = ROUNDTRIP_FIXTURES.story;
|
|
115
|
-
|
|
116
|
-
const docxRoundTrip = await docxToMarkdownDoc(await markdownDocToDocx(source.doc));
|
|
117
|
-
const pptxRoundTrip = await pptxToMarkdownDoc(await markdownDocToPptx(source.doc));
|
|
118
|
-
const pdfRoundTrip = await pdfToMarkdownDoc(await markdownDocToPdf(source.doc));
|
|
119
|
-
const csvRoundTrip = await csvToMarkdownDoc(markdownDocToCsv(ROUNDTRIP_FIXTURES.table.doc));
|
|
120
|
-
|
|
121
|
-
const docxText = extractNormalizedText(docxRoundTrip);
|
|
122
|
-
const pptxText = extractNormalizedText(pptxRoundTrip);
|
|
123
|
-
const pdfText = extractNormalizedText(pdfRoundTrip);
|
|
124
|
-
const csvText = extractNormalizedText(csvRoundTrip);
|
|
125
|
-
|
|
126
|
-
expect(docxText).toContain('river journal');
|
|
127
|
-
expect(pptxText).toContain('river journal');
|
|
128
|
-
expect(pdfText).toContain('river journal');
|
|
129
|
-
expect(csvText).toContain('throughput');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('round-trips markdown through XLSX with table cell survival', async () => {
|
|
133
|
-
const source = ROUNDTRIP_FIXTURES.table;
|
|
134
|
-
const xlsx = await markdownDocToXlsx(source.doc);
|
|
135
|
-
const roundTrip = await xlsxToMarkdownDoc(xlsx);
|
|
136
|
-
|
|
137
|
-
const text = extractNormalizedText(roundTrip);
|
|
138
|
-
for (const phrase of source.keyPhrases) {
|
|
139
|
-
expect(text).toContain(phrase);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the shared container-assembly tail (`shared/container.ts`) that the
|
|
3
|
-
* docx / pptx / pdf `*ToContainer` importers now delegate to.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import { buildContainer } from '../shared/container';
|
|
8
|
-
|
|
9
|
-
const BYTES_A = new Uint8Array([1, 2, 3, 4]);
|
|
10
|
-
const BYTES_B = new Uint8Array([9, 8, 7]);
|
|
11
|
-
|
|
12
|
-
describe('buildContainer', () => {
|
|
13
|
-
it('writes the markdown document and every image with its mime type', async () => {
|
|
14
|
-
const container = await buildContainer(
|
|
15
|
-
'# Hello\n\n',
|
|
16
|
-
new Map([
|
|
17
|
-
['images/a.png', { data: BYTES_A.buffer, mimeType: 'image/png' }],
|
|
18
|
-
['images/b.jpg', { data: BYTES_B.buffer, mimeType: 'image/jpeg' }],
|
|
19
|
-
]),
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
expect(await container.readDocument()).toContain('Hello');
|
|
23
|
-
expect(await container.exists('images/a.png')).toBe(true);
|
|
24
|
-
expect(await container.exists('images/b.jpg')).toBe(true);
|
|
25
|
-
expect(new Uint8Array((await container.readFile('images/a.png'))!)).toEqual(BYTES_A);
|
|
26
|
-
expect(new Uint8Array((await container.readFile('images/b.jpg'))!)).toEqual(BYTES_B);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('accepts any iterable of [path, {data, mimeType}] (e.g. mapped array)', async () => {
|
|
30
|
-
const images = [{ path: 'images/x.png', data: BYTES_A.buffer }].map(
|
|
31
|
-
(i) => [i.path, { data: i.data, mimeType: 'image/png' }] as const,
|
|
32
|
-
);
|
|
33
|
-
const container = await buildContainer('# Doc', images);
|
|
34
|
-
expect(await container.exists('images/x.png')).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('produces a valid empty container with no images', async () => {
|
|
38
|
-
const container = await buildContainer('', []);
|
|
39
|
-
expect(await container.readDocument()).toBe('');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the consolidated ext→MIME map (`shared/images.ts`) and the html
|
|
3
|
-
* `inferMimeType` wrapper that now delegates to it. This map is the union of
|
|
4
|
-
* the three previously-drifting copies (docx/pptx importers + html export).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import { extToMime } from '../shared/images';
|
|
9
|
-
import { inferMimeType } from '../html/imageUtils';
|
|
10
|
-
|
|
11
|
-
describe('extToMime', () => {
|
|
12
|
-
it('is dot-tolerant and case-insensitive', () => {
|
|
13
|
-
expect(extToMime('png')).toBe('image/png');
|
|
14
|
-
expect(extToMime('.png')).toBe('image/png');
|
|
15
|
-
expect(extToMime('.PNG')).toBe('image/png');
|
|
16
|
-
expect(extToMime('PNG')).toBe('image/png');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('resolves .avif to image/avif (the correct answer where the old maps disagreed)', () => {
|
|
20
|
-
// html's map had avif→image/avif; the docx/pptx maps omitted it entirely
|
|
21
|
-
// (would have fallen back to octet-stream). The union picks image/avif.
|
|
22
|
-
expect(extToMime('avif')).toBe('image/avif');
|
|
23
|
-
expect(extToMime('.avif')).toBe('image/avif');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('covers the union of all three previous maps', () => {
|
|
27
|
-
// Raster/vector (all three had most of these)
|
|
28
|
-
expect(extToMime('jpg')).toBe('image/jpeg');
|
|
29
|
-
expect(extToMime('jpeg')).toBe('image/jpeg');
|
|
30
|
-
expect(extToMime('gif')).toBe('image/gif');
|
|
31
|
-
expect(extToMime('webp')).toBe('image/webp');
|
|
32
|
-
expect(extToMime('bmp')).toBe('image/bmp');
|
|
33
|
-
expect(extToMime('svg')).toBe('image/svg+xml');
|
|
34
|
-
// Only docx/pptx had these
|
|
35
|
-
expect(extToMime('tiff')).toBe('image/tiff');
|
|
36
|
-
expect(extToMime('tif')).toBe('image/tiff');
|
|
37
|
-
expect(extToMime('emf')).toBe('image/emf');
|
|
38
|
-
expect(extToMime('wmf')).toBe('image/wmf');
|
|
39
|
-
// Only html had these
|
|
40
|
-
expect(extToMime('ico')).toBe('image/x-icon');
|
|
41
|
-
expect(extToMime('mp3')).toBe('audio/mpeg');
|
|
42
|
-
expect(extToMime('wav')).toBe('audio/wav');
|
|
43
|
-
expect(extToMime('ogg')).toBe('audio/ogg');
|
|
44
|
-
expect(extToMime('mp4')).toBe('video/mp4');
|
|
45
|
-
expect(extToMime('webm')).toBe('video/webm');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('returns application/octet-stream for unknown / empty extensions', () => {
|
|
49
|
-
expect(extToMime('xyz')).toBe('application/octet-stream');
|
|
50
|
-
expect(extToMime('')).toBe('application/octet-stream');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('inferMimeType (html wrapper)', () => {
|
|
55
|
-
it('delegates to extToMime, keying off the filename extension', () => {
|
|
56
|
-
expect(inferMimeType('images/hero.PNG')).toBe('image/png');
|
|
57
|
-
expect(inferMimeType('photo.avif')).toBe('image/avif');
|
|
58
|
-
expect(inferMimeType('clip.webm')).toBe('video/webm');
|
|
59
|
-
expect(inferMimeType('noextension')).toBe('application/octet-stream');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import JSZip from 'jszip';
|
|
3
|
-
import { parseMarkdown, type MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
4
|
-
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
5
|
-
|
|
6
|
-
import { markdownDocToXlsx, docToXlsx } from '../xlsx/export';
|
|
7
|
-
import { xlsxToMarkdownDoc } from '../xlsx/import';
|
|
8
|
-
|
|
9
|
-
/** Read the workbook.xml + all worksheet XML out of an exported .xlsx buffer. */
|
|
10
|
-
async function unzip(buffer: ArrayBuffer): Promise<{
|
|
11
|
-
workbook: string;
|
|
12
|
-
sheets: string[];
|
|
13
|
-
paths: string[];
|
|
14
|
-
}> {
|
|
15
|
-
const zip = await JSZip.loadAsync(buffer);
|
|
16
|
-
const paths = Object.keys(zip.files).sort();
|
|
17
|
-
const workbook = (await zip.file('xl/workbook.xml')?.async('string')) ?? '';
|
|
18
|
-
const sheetPaths = paths.filter((p) => /^xl\/worksheets\/sheet\d+\.xml$/.test(p));
|
|
19
|
-
const sheets = await Promise.all(sheetPaths.map((p) => zip.file(p)!.async('string')));
|
|
20
|
-
return { workbook, sheets, paths };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Extract sheet names (in order) from a workbook.xml string. */
|
|
24
|
-
function sheetNames(workbook: string): string[] {
|
|
25
|
-
return [...workbook.matchAll(/<sheet\s+name="([^"]*)"/g)].map((m) => m[1]!);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Build a single-heading + single-table document without touching the parser. */
|
|
29
|
-
function docWithHeading(headingText: string): MarkdownDocument {
|
|
30
|
-
return {
|
|
31
|
-
type: 'document',
|
|
32
|
-
children: [
|
|
33
|
-
{ type: 'heading', depth: 1, children: [{ type: 'text', value: headingText }] },
|
|
34
|
-
{
|
|
35
|
-
type: 'table',
|
|
36
|
-
children: [
|
|
37
|
-
{
|
|
38
|
-
type: 'tableRow',
|
|
39
|
-
children: [
|
|
40
|
-
{ type: 'tableCell', isHeader: true, children: [{ type: 'text', value: 'A' }] },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
type: 'tableRow',
|
|
45
|
-
children: [{ type: 'tableCell', children: [{ type: 'text', value: 'x' }] }],
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('markdownDocToXlsx', () => {
|
|
54
|
-
it('produces a structurally valid package (content types + rels + workbook)', async () => {
|
|
55
|
-
const md = parseMarkdown('# Data\n\n| A | B |\n| - | - |\n| 1 | 2 |\n');
|
|
56
|
-
const buffer = await markdownDocToXlsx(md);
|
|
57
|
-
const { paths } = await unzip(buffer);
|
|
58
|
-
|
|
59
|
-
expect(paths).toContain('[Content_Types].xml');
|
|
60
|
-
expect(paths).toContain('_rels/.rels');
|
|
61
|
-
expect(paths).toContain('xl/workbook.xml');
|
|
62
|
-
expect(paths).toContain('xl/_rels/workbook.xml.rels');
|
|
63
|
-
expect(paths).toContain('xl/worksheets/sheet1.xml');
|
|
64
|
-
expect(paths).toContain('xl/styles.xml');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('names each sheet from the nearest preceding heading', async () => {
|
|
68
|
-
const md = parseMarkdown('# Sales\n\n| A |\n| - |\n| x |\n');
|
|
69
|
-
const { workbook } = await unzip(await markdownDocToXlsx(md));
|
|
70
|
-
expect(sheetNames(workbook)).toEqual(['Sales']);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('sanitizes invalid Excel characters and caps names at 31 chars', async () => {
|
|
74
|
-
const { workbook } = await unzip(await markdownDocToXlsx(docWithHeading('a:b/c\\d[e]f?g*h')));
|
|
75
|
-
expect(sheetNames(workbook)).toEqual(['abcdefgh']);
|
|
76
|
-
|
|
77
|
-
const { workbook: wb2 } = await unzip(await markdownDocToXlsx(docWithHeading('X'.repeat(50))));
|
|
78
|
-
expect(sheetNames(wb2)[0]!.length).toBe(31);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('de-duplicates repeated sheet names (Name, Name2, Name3, …)', async () => {
|
|
82
|
-
const md = parseMarkdown(
|
|
83
|
-
'# Report\n\n| A |\n| - |\n| 1 |\n\n# Report\n\n| B |\n| - |\n| 2 |\n\n# Report\n\n| C |\n| - |\n| 3 |\n',
|
|
84
|
-
);
|
|
85
|
-
const { workbook } = await unzip(await markdownDocToXlsx(md));
|
|
86
|
-
expect(sheetNames(workbook)).toEqual(['Report', 'Report2', 'Report3']);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('falls back to Sheet1, Sheet2 when a table has no preceding heading', async () => {
|
|
90
|
-
const md = parseMarkdown('| A |\n| - |\n| 1 |\n\n| B |\n| - |\n| 2 |\n');
|
|
91
|
-
const { workbook } = await unzip(await markdownDocToXlsx(md));
|
|
92
|
-
expect(sheetNames(workbook)).toEqual(['Sheet1', 'Sheet2']);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('emits plain numbers as numeric cells and everything else as inline strings', async () => {
|
|
96
|
-
const md = parseMarkdown(
|
|
97
|
-
'# T\n\n| Label | N |\n| - | - |\n| Alpha | 42 |\n| Neg | -3.5 |\n| Mixed | 120 req/s |\n',
|
|
98
|
-
);
|
|
99
|
-
const { sheets } = await unzip(await markdownDocToXlsx(md));
|
|
100
|
-
const xml = sheets[0]!;
|
|
101
|
-
|
|
102
|
-
// Numeric cells: <v>…</v> without t="inlineStr"
|
|
103
|
-
expect(xml).toContain('<v>42</v>');
|
|
104
|
-
expect(xml).toContain('<v>-3.5</v>');
|
|
105
|
-
// String cells: inlineStr
|
|
106
|
-
expect(xml).toContain('t="inlineStr"');
|
|
107
|
-
expect(xml).toContain('>Alpha<');
|
|
108
|
-
expect(xml).toContain('>120 req/s<');
|
|
109
|
-
// "120 req/s" must NOT be numeric
|
|
110
|
-
expect(xml).not.toContain('<v>120 req/s</v>');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('maps multiple tables to multiple worksheets', async () => {
|
|
114
|
-
const md = parseMarkdown('# First\n\n| A |\n| - |\n| 1 |\n\n# Second\n\n| B |\n| - |\n| 2 |\n');
|
|
115
|
-
const { sheets, workbook } = await unzip(await markdownDocToXlsx(md));
|
|
116
|
-
expect(sheets.length).toBe(2);
|
|
117
|
-
expect(sheetNames(workbook)).toEqual(['First', 'Second']);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('produces a valid empty file (single sheet) when there are no tables', async () => {
|
|
121
|
-
const md = parseMarkdown('# Just prose\n\nNo tables here at all.\n');
|
|
122
|
-
const buffer = await markdownDocToXlsx(md);
|
|
123
|
-
const { sheets, workbook } = await unzip(buffer);
|
|
124
|
-
expect(sheets.length).toBe(1);
|
|
125
|
-
expect(sheetNames(workbook)).toEqual(['Sheet1']);
|
|
126
|
-
|
|
127
|
-
// Still round-trips without throwing.
|
|
128
|
-
const roundTrip = await xlsxToMarkdownDoc(buffer);
|
|
129
|
-
expect(roundTrip.type).toBe('document');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('honors a custom sheetNamePrefix for auto-named sheets', async () => {
|
|
133
|
-
const md = parseMarkdown('| A |\n| - |\n| 1 |\n');
|
|
134
|
-
const { workbook } = await unzip(await markdownDocToXlsx(md, { sheetNamePrefix: 'Tab' }));
|
|
135
|
-
expect(sheetNames(workbook)).toEqual(['Tab1']);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('survives a full round-trip (cell text preserved through import)', async () => {
|
|
139
|
-
const md = parseMarkdown(
|
|
140
|
-
'# Metrics\n\n| Metric | Value |\n| - | - |\n| Throughput | 120 |\n| Errors | 0.2% |\n',
|
|
141
|
-
);
|
|
142
|
-
const roundTrip = await xlsxToMarkdownDoc(await markdownDocToXlsx(md));
|
|
143
|
-
const table = roundTrip.children.find((n) => n.type === 'table');
|
|
144
|
-
expect(table).toBeDefined();
|
|
145
|
-
|
|
146
|
-
const flat = JSON.stringify(roundTrip);
|
|
147
|
-
for (const cell of ['Metric', 'Value', 'Throughput', '120', 'Errors', '0.2%']) {
|
|
148
|
-
expect(flat).toContain(cell);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('docToXlsx converts a Doc via the markdown table model', async () => {
|
|
153
|
-
const doc = markdownToDoc(parseMarkdown('# Report\n\n| K | V |\n| - | - |\n| Users | 99 |\n'));
|
|
154
|
-
const buffer = await docToXlsx(doc);
|
|
155
|
-
const { sheets, workbook } = await unzip(buffer);
|
|
156
|
-
expect(sheets.length).toBeGreaterThanOrEqual(1);
|
|
157
|
-
expect(sheetNames(workbook).length).toBeGreaterThanOrEqual(1);
|
|
158
|
-
|
|
159
|
-
const roundTrip = await xlsxToMarkdownDoc(buffer);
|
|
160
|
-
const flat = JSON.stringify(roundTrip);
|
|
161
|
-
expect(flat).toContain('Users');
|
|
162
|
-
expect(flat).toContain('99');
|
|
163
|
-
});
|
|
164
|
-
});
|