@bendyline/squisq-formats 2.0.1 → 2.1.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-2KPARF2P.js → chunk-74GO3FVS.js} +3 -3
- package/dist/{chunk-WC7WULGV.js → chunk-7ARKUCQT.js} +9 -1
- package/dist/chunk-7ARKUCQT.js.map +1 -0
- package/dist/{chunk-LXYLOOST.js → chunk-DTDF6QDP.js} +3 -3
- package/dist/chunk-DTDF6QDP.js.map +1 -0
- package/dist/{chunk-QRVN6A6E.js → chunk-EW54IRRS.js} +4 -4
- package/dist/{chunk-U32AG3G3.js → chunk-KURGXM4I.js} +2 -2
- package/dist/{chunk-CRAVSMPZ.js → chunk-NNHKUXKA.js} +292 -70
- package/dist/chunk-NNHKUXKA.js.map +1 -0
- package/dist/{chunk-VSYHZECT.js → chunk-QFLDYKCR.js} +2 -2
- package/dist/{chunk-ABVI556T.js → chunk-RLU7UFYU.js} +5 -5
- package/dist/{chunk-XKUMNGBW.js → chunk-SC67HYQJ.js} +2 -2
- package/dist/{chunk-VJJM2SSH.js → chunk-UGYF5AZE.js} +3 -3
- package/dist/{chunk-HTW2M27H.js → chunk-WQSHGBLN.js} +1 -1
- package/dist/docx/index.js +4 -3
- package/dist/{extract-H6RXJMHP.js → extract-OJ7ZQV6P.js} +4 -4
- package/dist/html/index.js +1 -1
- package/dist/index.js +11 -11
- package/dist/infer/index.js +4 -4
- package/dist/{layouts-QVPK3ZCU.js → layouts-5VDIRPIJ.js} +3 -3
- package/dist/ooxml/index.js +3 -3
- package/dist/pptx/index.js +4 -4
- package/dist/registry/index.js +1 -1
- package/dist/xlsx/index.js +3 -3
- package/package.json +2 -2
- package/src/__tests__/docxExport.test.ts +34 -0
- package/src/__tests__/docxImport.test.ts +121 -0
- package/src/__tests__/html.test.ts +4 -0
- package/src/docx/export.ts +113 -5
- package/src/docx/import.ts +324 -69
- package/src/html/htmlTemplate.ts +2 -2
- package/src/ooxml/namespaces.ts +21 -0
- package/dist/chunk-CRAVSMPZ.js.map +0 -1
- package/dist/chunk-LXYLOOST.js.map +0 -1
- package/dist/chunk-WC7WULGV.js.map +0 -1
- /package/dist/{chunk-2KPARF2P.js.map → chunk-74GO3FVS.js.map} +0 -0
- /package/dist/{chunk-QRVN6A6E.js.map → chunk-EW54IRRS.js.map} +0 -0
- /package/dist/{chunk-U32AG3G3.js.map → chunk-KURGXM4I.js.map} +0 -0
- /package/dist/{chunk-VSYHZECT.js.map → chunk-QFLDYKCR.js.map} +0 -0
- /package/dist/{chunk-ABVI556T.js.map → chunk-RLU7UFYU.js.map} +0 -0
- /package/dist/{chunk-XKUMNGBW.js.map → chunk-SC67HYQJ.js.map} +0 -0
- /package/dist/{chunk-VJJM2SSH.js.map → chunk-UGYF5AZE.js.map} +0 -0
- /package/dist/{chunk-HTW2M27H.js.map → chunk-WQSHGBLN.js.map} +0 -0
- /package/dist/{extract-H6RXJMHP.js.map → extract-OJ7ZQV6P.js.map} +0 -0
- /package/dist/{layouts-QVPK3ZCU.js.map → layouts-5VDIRPIJ.js.map} +0 -0
package/dist/pptx/index.js
CHANGED
|
@@ -4,17 +4,17 @@ import {
|
|
|
4
4
|
pptxToContainer,
|
|
5
5
|
pptxToDoc,
|
|
6
6
|
pptxToMarkdownDoc
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-RLU7UFYU.js";
|
|
8
8
|
import {
|
|
9
9
|
analyzePptxLayouts,
|
|
10
10
|
inspectPptxLayouts
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-QFLDYKCR.js";
|
|
12
12
|
import "../chunk-6M7Z25LA.js";
|
|
13
13
|
import "../chunk-PN52A5AA.js";
|
|
14
14
|
import "../chunk-YRT7GQ5Y.js";
|
|
15
|
-
import "../chunk-
|
|
15
|
+
import "../chunk-KURGXM4I.js";
|
|
16
16
|
import "../chunk-U4MRIFKL.js";
|
|
17
|
-
import "../chunk-
|
|
17
|
+
import "../chunk-7ARKUCQT.js";
|
|
18
18
|
import "../chunk-FE6OJV6O.js";
|
|
19
19
|
import "../chunk-SSUPBUF5.js";
|
|
20
20
|
import "../chunk-MLX2BOJC.js";
|
package/dist/registry/index.js
CHANGED
package/dist/xlsx/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
markdownDocToXlsx,
|
|
4
4
|
xlsxToDoc,
|
|
5
5
|
xlsxToMarkdownDoc
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-UGYF5AZE.js";
|
|
7
7
|
import "../chunk-PN52A5AA.js";
|
|
8
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-KURGXM4I.js";
|
|
9
9
|
import "../chunk-U4MRIFKL.js";
|
|
10
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-7ARKUCQT.js";
|
|
11
11
|
import "../chunk-FE6OJV6O.js";
|
|
12
12
|
export {
|
|
13
13
|
docToXlsx,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-formats",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Document format converters — DOCX, PDF, OOXML import/export",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
102
|
"@xmldom/xmldom": "0.9.10",
|
|
103
|
-
"@bendyline/squisq": "2.0
|
|
103
|
+
"@bendyline/squisq": "2.1.0",
|
|
104
104
|
"jszip": "3.10.1",
|
|
105
105
|
"pdf-lib": "1.17.1",
|
|
106
106
|
"pdfjs-dist": "4.10.38"
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
MarkdownEmphasis,
|
|
23
23
|
MarkdownInlineCode,
|
|
24
24
|
MarkdownLink,
|
|
25
|
+
MarkdownContainerDirective,
|
|
25
26
|
} from '@bendyline/squisq/markdown';
|
|
26
27
|
|
|
27
28
|
import { markdownDocToDocx, docToDocx } from '../docx/export';
|
|
@@ -81,6 +82,39 @@ describe('markdownDocToDocx', () => {
|
|
|
81
82
|
expect(body).toBeDefined();
|
|
82
83
|
});
|
|
83
84
|
|
|
85
|
+
it('exports DOCX story directives to header and footer parts', async () => {
|
|
86
|
+
const doc: MarkdownDocument = {
|
|
87
|
+
type: 'document',
|
|
88
|
+
children: [
|
|
89
|
+
{ type: 'paragraph', children: [{ type: 'text', value: 'Body' }] },
|
|
90
|
+
{
|
|
91
|
+
type: 'containerDirective',
|
|
92
|
+
name: 'docx-header',
|
|
93
|
+
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Header' }] }],
|
|
94
|
+
} satisfies MarkdownContainerDirective,
|
|
95
|
+
{
|
|
96
|
+
type: 'containerDirective',
|
|
97
|
+
name: 'docx-footer',
|
|
98
|
+
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Footer' }] }],
|
|
99
|
+
} satisfies MarkdownContainerDirective,
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const zip = await exportAndParse(doc);
|
|
104
|
+
const documentXml = await zip.file('word/document.xml')!.async('text');
|
|
105
|
+
const headerXml = await zip.file('word/header1.xml')!.async('text');
|
|
106
|
+
const footerXml = await zip.file('word/footer1.xml')!.async('text');
|
|
107
|
+
const relationships = await zip.file('word/_rels/document.xml.rels')!.async('text');
|
|
108
|
+
|
|
109
|
+
expect(documentXml).toContain('<w:headerReference');
|
|
110
|
+
expect(documentXml).toContain('<w:footerReference');
|
|
111
|
+
expect(documentXml).not.toContain('Header');
|
|
112
|
+
expect(headerXml).toContain('Header');
|
|
113
|
+
expect(footerXml).toContain('Footer');
|
|
114
|
+
expect(relationships).toContain('/relationships/header');
|
|
115
|
+
expect(relationships).toContain('/relationships/footer');
|
|
116
|
+
});
|
|
117
|
+
|
|
84
118
|
// ============================================
|
|
85
119
|
// Headings
|
|
86
120
|
// ============================================
|
|
@@ -14,9 +14,13 @@ import {
|
|
|
14
14
|
REL_STYLES,
|
|
15
15
|
REL_NUMBERING,
|
|
16
16
|
REL_HYPERLINK,
|
|
17
|
+
REL_HEADER,
|
|
18
|
+
REL_FOOTER,
|
|
17
19
|
CONTENT_TYPE_DOCX_DOCUMENT,
|
|
18
20
|
CONTENT_TYPE_DOCX_STYLES,
|
|
19
21
|
CONTENT_TYPE_DOCX_NUMBERING,
|
|
22
|
+
CONTENT_TYPE_DOCX_HEADER,
|
|
23
|
+
CONTENT_TYPE_DOCX_FOOTER,
|
|
20
24
|
} from '../ooxml/namespaces';
|
|
21
25
|
import { xmlDeclaration } from '../ooxml/xmlUtils';
|
|
22
26
|
import { docxToMarkdownDoc, docxToDoc } from '../docx/import';
|
|
@@ -31,6 +35,7 @@ import type {
|
|
|
31
35
|
MarkdownStrikethrough,
|
|
32
36
|
MarkdownLink,
|
|
33
37
|
MarkdownInlineNode,
|
|
38
|
+
MarkdownContainerDirective,
|
|
34
39
|
} from '@bendyline/squisq/markdown';
|
|
35
40
|
|
|
36
41
|
// ============================================
|
|
@@ -47,6 +52,7 @@ interface DocxFixtureOptions {
|
|
|
47
52
|
target: string;
|
|
48
53
|
targetMode?: 'External';
|
|
49
54
|
}>;
|
|
55
|
+
extraParts?: Array<{ path: string; content: string; contentType: string }>;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
async function buildTestDocx(options: DocxFixtureOptions): Promise<ArrayBuffer> {
|
|
@@ -81,6 +87,10 @@ async function buildTestDocx(options: DocxFixtureOptions): Promise<ArrayBuffer>
|
|
|
81
87
|
pkg.addPart('word/numbering.xml', options.numberingXml, CONTENT_TYPE_DOCX_NUMBERING);
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
for (const part of options.extraParts ?? []) {
|
|
91
|
+
pkg.addPart(part.path, part.content, part.contentType);
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
// Root relationship
|
|
85
95
|
pkg.addRelationship('', {
|
|
86
96
|
id: 'rId1',
|
|
@@ -164,6 +174,25 @@ describe('docxToMarkdownDoc', () => {
|
|
|
164
174
|
);
|
|
165
175
|
});
|
|
166
176
|
|
|
177
|
+
it('imports visible content inside controls, revisions, and text boxes once', async () => {
|
|
178
|
+
const data = await buildTestDocx({
|
|
179
|
+
bodyXml:
|
|
180
|
+
`<w:p>` +
|
|
181
|
+
`<w:r><w:t>Body </w:t></w:r>` +
|
|
182
|
+
`<w:sdt><w:sdtContent><w:r><w:t>controlled </w:t></w:r></w:sdtContent></w:sdt>` +
|
|
183
|
+
`<w:ins><w:r><w:t>inserted </w:t></w:r></w:ins>` +
|
|
184
|
+
`<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">` +
|
|
185
|
+
`<mc:Choice><w:drawing><w:txbxContent><w:p><w:r><w:t>text box</w:t></w:r></w:p></w:txbxContent></w:drawing></mc:Choice>` +
|
|
186
|
+
`<mc:Fallback><w:pict><w:txbxContent><w:p><w:r><w:t>fallback duplicate</w:t></w:r></w:p></w:txbxContent></w:pict></mc:Fallback>` +
|
|
187
|
+
`</mc:AlternateContent>` +
|
|
188
|
+
`</w:p>`,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const paragraph = docText(await docxToMarkdownDoc(data));
|
|
192
|
+
expect(paragraph).toContain('Body controlled inserted text box');
|
|
193
|
+
expect(paragraph).not.toContain('fallback duplicate');
|
|
194
|
+
});
|
|
195
|
+
|
|
167
196
|
it('skips empty paragraphs', async () => {
|
|
168
197
|
const data = await buildTestDocx({
|
|
169
198
|
bodyXml: `<w:p><w:pPr/></w:p><w:p><w:r><w:t>Text</w:t></w:r></w:p>`,
|
|
@@ -331,6 +360,27 @@ describe('docxToMarkdownDoc', () => {
|
|
|
331
360
|
expect(list.ordered).toBe(true);
|
|
332
361
|
});
|
|
333
362
|
|
|
363
|
+
it('retains nested list text after an empty parent numbering paragraph', async () => {
|
|
364
|
+
const numberingXml =
|
|
365
|
+
xmlDeclaration() +
|
|
366
|
+
`<w:numbering xmlns:w="${NS_WML}">` +
|
|
367
|
+
`<w:abstractNum w:abstractNumId="3">` +
|
|
368
|
+
`<w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/></w:lvl>` +
|
|
369
|
+
`<w:lvl w:ilvl="1"><w:numFmt w:val="decimal"/></w:lvl>` +
|
|
370
|
+
`</w:abstractNum>` +
|
|
371
|
+
`<w:num w:numId="3"><w:abstractNumId w:val="3"/></w:num>` +
|
|
372
|
+
`</w:numbering>`;
|
|
373
|
+
const data = await buildTestDocx({
|
|
374
|
+
bodyXml:
|
|
375
|
+
`<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="3"/></w:numPr></w:pPr></w:p>` +
|
|
376
|
+
`<w:p><w:pPr><w:numPr><w:ilvl w:val="1"/><w:numId w:val="3"/></w:numPr></w:pPr>` +
|
|
377
|
+
`<w:r><w:t>Nested content survives</w:t></w:r></w:p>`,
|
|
378
|
+
numberingXml,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(docText(await docxToMarkdownDoc(data))).toContain('Nested content survives');
|
|
382
|
+
});
|
|
383
|
+
|
|
334
384
|
// ============================================
|
|
335
385
|
// Tables
|
|
336
386
|
// ============================================
|
|
@@ -363,6 +413,57 @@ describe('docxToMarkdownDoc', () => {
|
|
|
363
413
|
expect((cell10.children[0] as MarkdownText).value).toBe('1');
|
|
364
414
|
});
|
|
365
415
|
|
|
416
|
+
it('flattens nested table contents without losing their text', async () => {
|
|
417
|
+
const data = await buildTestDocx({
|
|
418
|
+
bodyXml:
|
|
419
|
+
`<w:tbl><w:tr><w:tc>` +
|
|
420
|
+
`<w:p><w:r><w:t>Outer</w:t></w:r></w:p>` +
|
|
421
|
+
`<w:tbl><w:tr>` +
|
|
422
|
+
`<w:tc><w:p><w:r><w:t>Nested A</w:t></w:r></w:p></w:tc>` +
|
|
423
|
+
`<w:tc><w:p><w:r><w:t>Nested B</w:t></w:r></w:p></w:tc>` +
|
|
424
|
+
`</w:tr></w:tbl>` +
|
|
425
|
+
`</w:tc></w:tr></w:tbl>`,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(docText(await docxToMarkdownDoc(data))).toContain('Outer Nested A Nested B');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('imports headers and footers as explicit DOCX story directives', async () => {
|
|
432
|
+
const headerXml =
|
|
433
|
+
xmlDeclaration() +
|
|
434
|
+
`<w:hdr xmlns:w="${NS_WML}"><w:p><w:r><w:t>Header text</w:t></w:r></w:p></w:hdr>`;
|
|
435
|
+
const footerXml =
|
|
436
|
+
xmlDeclaration() +
|
|
437
|
+
`<w:ftr xmlns:w="${NS_WML}"><w:p><w:r><w:t>Footer text</w:t></w:r></w:p></w:ftr>`;
|
|
438
|
+
const data = await buildTestDocx({
|
|
439
|
+
bodyXml:
|
|
440
|
+
`<w:p><w:r><w:t>Body text</w:t></w:r></w:p>` +
|
|
441
|
+
`<w:sectPr><w:headerReference w:type="default" r:id="rIdHeader"/>` +
|
|
442
|
+
`<w:footerReference w:type="default" r:id="rIdFooter"/></w:sectPr>`,
|
|
443
|
+
documentRels: [
|
|
444
|
+
{ id: 'rIdHeader', type: REL_HEADER, target: 'header1.xml' },
|
|
445
|
+
{ id: 'rIdFooter', type: REL_FOOTER, target: 'footer1.xml' },
|
|
446
|
+
],
|
|
447
|
+
extraParts: [
|
|
448
|
+
{ path: 'word/header1.xml', content: headerXml, contentType: CONTENT_TYPE_DOCX_HEADER },
|
|
449
|
+
{ path: 'word/footer1.xml', content: footerXml, contentType: CONTENT_TYPE_DOCX_FOOTER },
|
|
450
|
+
],
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const doc = await docxToMarkdownDoc(data);
|
|
454
|
+
const header = doc.children.find(
|
|
455
|
+
(node): node is MarkdownContainerDirective =>
|
|
456
|
+
node.type === 'containerDirective' && node.name === 'docx-header',
|
|
457
|
+
);
|
|
458
|
+
const footer = doc.children.find(
|
|
459
|
+
(node): node is MarkdownContainerDirective =>
|
|
460
|
+
node.type === 'containerDirective' && node.name === 'docx-footer',
|
|
461
|
+
);
|
|
462
|
+
expect(header?.attributes?.type).toBe('default');
|
|
463
|
+
expect(footer?.attributes?.type).toBe('default');
|
|
464
|
+
expect(docText(doc)).toContain('Body text Header text Footer text');
|
|
465
|
+
});
|
|
466
|
+
|
|
366
467
|
// ============================================
|
|
367
468
|
// Empty Document
|
|
368
469
|
// ============================================
|
|
@@ -387,8 +488,28 @@ describe('docxToMarkdownDoc', () => {
|
|
|
387
488
|
const para = doc.children[0] as MarkdownParagraph;
|
|
388
489
|
expect(para.children.some((c: MarkdownInlineNode) => c.type === 'break')).toBe(true);
|
|
389
490
|
});
|
|
491
|
+
|
|
492
|
+
it('retains tabs as visible word boundaries', async () => {
|
|
493
|
+
const data = await buildTestDocx({
|
|
494
|
+
bodyXml: `<w:p><w:r><w:t>Before</w:t><w:tab/><w:t>After</w:t></w:r></w:p>`,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(docText(await docxToMarkdownDoc(data))).toBe('Before After');
|
|
498
|
+
});
|
|
390
499
|
});
|
|
391
500
|
|
|
501
|
+
function docText(doc: Awaited<ReturnType<typeof docxToMarkdownDoc>>): string {
|
|
502
|
+
const text: string[] = [];
|
|
503
|
+
const visit = (node: unknown): void => {
|
|
504
|
+
if (!node || typeof node !== 'object') return;
|
|
505
|
+
const value = node as { type?: string; value?: string; children?: unknown[] };
|
|
506
|
+
if ((value.type === 'text' || value.type === 'code') && value.value) text.push(value.value);
|
|
507
|
+
value.children?.forEach(visit);
|
|
508
|
+
};
|
|
509
|
+
visit(doc);
|
|
510
|
+
return text.join(' ').replace(/\s+/g, ' ').trim();
|
|
511
|
+
}
|
|
512
|
+
|
|
392
513
|
// ============================================
|
|
393
514
|
// docxToDoc convenience wrapper
|
|
394
515
|
// ============================================
|
|
@@ -300,6 +300,10 @@ describe('docToHtml', () => {
|
|
|
300
300
|
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT, mode: 'static' });
|
|
301
301
|
|
|
302
302
|
expect(html).toContain('"static"');
|
|
303
|
+
// The page rendition owns its own background/scrolling — the host shim
|
|
304
|
+
// must not hardcode a white background that fights themed pages.
|
|
305
|
+
expect(html).toContain('#squisq-root{display:block}');
|
|
306
|
+
expect(html).not.toContain('background:#fff');
|
|
303
307
|
});
|
|
304
308
|
|
|
305
309
|
it('inlines images as base64 data URIs', () => {
|
package/src/docx/export.ts
CHANGED
|
@@ -62,12 +62,16 @@ import {
|
|
|
62
62
|
REL_HYPERLINK,
|
|
63
63
|
REL_IMAGE,
|
|
64
64
|
REL_FOOTNOTES,
|
|
65
|
+
REL_HEADER,
|
|
66
|
+
REL_FOOTER,
|
|
65
67
|
CONTENT_TYPE_DOCX_DOCUMENT,
|
|
66
68
|
CONTENT_TYPE_DOCX_STYLES,
|
|
67
69
|
CONTENT_TYPE_DOCX_NUMBERING,
|
|
68
70
|
CONTENT_TYPE_DOCX_SETTINGS,
|
|
69
71
|
CONTENT_TYPE_DOCX_FONT_TABLE,
|
|
70
72
|
CONTENT_TYPE_DOCX_FOOTNOTES,
|
|
73
|
+
CONTENT_TYPE_DOCX_HEADER,
|
|
74
|
+
CONTENT_TYPE_DOCX_FOOTER,
|
|
71
75
|
} from '../ooxml/namespaces.js';
|
|
72
76
|
import {
|
|
73
77
|
DEPTH_TO_STYLE_ID,
|
|
@@ -135,8 +139,35 @@ export async function markdownDocToDocx(
|
|
|
135
139
|
? options
|
|
136
140
|
: { ...options, themeId: readFrontmatterThemeId(doc.frontmatter) };
|
|
137
141
|
const ctx = new ExportContext(resolvedOptions, doc);
|
|
138
|
-
const
|
|
139
|
-
|
|
142
|
+
const stories = partitionDocumentStories(doc.children);
|
|
143
|
+
ctx.setRelationshipSource('document');
|
|
144
|
+
const bodyXml = convertBlocks(stories.body, ctx);
|
|
145
|
+
ctx.setRelationshipSource('header');
|
|
146
|
+
const headerXml = convertBlocks(stories.header, ctx);
|
|
147
|
+
ctx.setRelationshipSource('footer');
|
|
148
|
+
const footerXml = convertBlocks(stories.footer, ctx);
|
|
149
|
+
ctx.setRelationshipSource('document');
|
|
150
|
+
return buildDocxPackage(bodyXml, headerXml, footerXml, ctx, resolvedOptions);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface DocumentStories {
|
|
154
|
+
body: MarkdownBlockNode[];
|
|
155
|
+
header: MarkdownBlockNode[];
|
|
156
|
+
footer: MarkdownBlockNode[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function partitionDocumentStories(nodes: MarkdownBlockNode[]): DocumentStories {
|
|
160
|
+
const stories: DocumentStories = { body: [], header: [], footer: [] };
|
|
161
|
+
for (const node of nodes) {
|
|
162
|
+
if (node.type === 'containerDirective' && node.name === 'docx-header') {
|
|
163
|
+
stories.header.push(...node.children);
|
|
164
|
+
} else if (node.type === 'containerDirective' && node.name === 'docx-footer') {
|
|
165
|
+
stories.footer.push(...node.children);
|
|
166
|
+
} else {
|
|
167
|
+
stories.body.push(node);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return stories;
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
/**
|
|
@@ -172,8 +203,11 @@ class ExportContext {
|
|
|
172
203
|
type: string;
|
|
173
204
|
target: string;
|
|
174
205
|
targetMode?: 'External';
|
|
206
|
+
source: DocxStoryPart;
|
|
175
207
|
}> = [];
|
|
176
208
|
|
|
209
|
+
private relationshipSource: DocxStoryPart = 'document';
|
|
210
|
+
|
|
177
211
|
/** Numbering definitions (abstract + num) */
|
|
178
212
|
readonly numberingDefs: NumberingDef[] = [];
|
|
179
213
|
|
|
@@ -257,6 +291,10 @@ class ExportContext {
|
|
|
257
291
|
return `rId${this.nextRelId++}`;
|
|
258
292
|
}
|
|
259
293
|
|
|
294
|
+
setRelationshipSource(source: DocxStoryPart): void {
|
|
295
|
+
this.relationshipSource = source;
|
|
296
|
+
}
|
|
297
|
+
|
|
260
298
|
/** Add a hyperlink relationship and return the rId */
|
|
261
299
|
addHyperlink(url: string): string {
|
|
262
300
|
const id = this.allocRelId();
|
|
@@ -265,6 +303,7 @@ class ExportContext {
|
|
|
265
303
|
type: REL_HYPERLINK,
|
|
266
304
|
target: url,
|
|
267
305
|
targetMode: 'External',
|
|
306
|
+
source: this.relationshipSource,
|
|
268
307
|
});
|
|
269
308
|
return id;
|
|
270
309
|
}
|
|
@@ -284,6 +323,7 @@ class ExportContext {
|
|
|
284
323
|
id: relId,
|
|
285
324
|
type: REL_IMAGE,
|
|
286
325
|
target: `media/${filename}`,
|
|
326
|
+
source: this.relationshipSource,
|
|
287
327
|
});
|
|
288
328
|
|
|
289
329
|
return { relId, docPrId };
|
|
@@ -309,6 +349,8 @@ class ExportContext {
|
|
|
309
349
|
}
|
|
310
350
|
}
|
|
311
351
|
|
|
352
|
+
type DocxStoryPart = 'document' | 'header' | 'footer';
|
|
353
|
+
|
|
312
354
|
interface NumberingDef {
|
|
313
355
|
numId: number;
|
|
314
356
|
ordered: boolean;
|
|
@@ -384,6 +426,10 @@ function convertBlock(node: MarkdownBlockNode, ctx: ExportContext, listDepth: nu
|
|
|
384
426
|
return convertMathBlock(node);
|
|
385
427
|
case 'footnoteDefinition':
|
|
386
428
|
return convertFootnoteDefinition(node, ctx);
|
|
429
|
+
case 'containerDirective':
|
|
430
|
+
// Non-DOCX directives have no native Word wrapper. Preserve their
|
|
431
|
+
// contents rather than silently dropping the entire subtree.
|
|
432
|
+
return convertBlocks(node.children, ctx);
|
|
387
433
|
default:
|
|
388
434
|
// Definition lists, directives, link definitions — skip or emit as plain text
|
|
389
435
|
return '';
|
|
@@ -858,6 +904,8 @@ function convertFootnoteRef(node: MarkdownFootnoteReference, ctx: ExportContext)
|
|
|
858
904
|
|
|
859
905
|
async function buildDocxPackage(
|
|
860
906
|
bodyXml: string,
|
|
907
|
+
headerXml: string,
|
|
908
|
+
footerXml: string,
|
|
861
909
|
ctx: ExportContext,
|
|
862
910
|
options: DocxExportOptions,
|
|
863
911
|
): Promise<ArrayBuffer> {
|
|
@@ -870,11 +918,27 @@ async function buildDocxPackage(
|
|
|
870
918
|
const settingsRelId = `rId${relCounter++}`;
|
|
871
919
|
const fontTableRelId = `rId${relCounter++}`;
|
|
872
920
|
const footnotesRelId = `rId${relCounter++}`;
|
|
921
|
+
const headerRelId = `rId${relCounter++}`;
|
|
922
|
+
const footerRelId = `rId${relCounter++}`;
|
|
923
|
+
const hasHeader = headerXml.trim().length > 0;
|
|
924
|
+
const hasFooter = footerXml.trim().length > 0;
|
|
873
925
|
|
|
874
926
|
// --- word/document.xml ---
|
|
875
|
-
const documentXml = buildDocumentXml(
|
|
927
|
+
const documentXml = buildDocumentXml(
|
|
928
|
+
bodyXml,
|
|
929
|
+
ctx.backgroundColor,
|
|
930
|
+
hasHeader ? headerRelId : undefined,
|
|
931
|
+
hasFooter ? footerRelId : undefined,
|
|
932
|
+
);
|
|
876
933
|
pkg.addPart('word/document.xml', documentXml, CONTENT_TYPE_DOCX_DOCUMENT);
|
|
877
934
|
|
|
935
|
+
if (hasHeader) {
|
|
936
|
+
pkg.addPart('word/header1.xml', buildStoryXml('header', headerXml), CONTENT_TYPE_DOCX_HEADER);
|
|
937
|
+
}
|
|
938
|
+
if (hasFooter) {
|
|
939
|
+
pkg.addPart('word/footer1.xml', buildStoryXml('footer', footerXml), CONTENT_TYPE_DOCX_FOOTER);
|
|
940
|
+
}
|
|
941
|
+
|
|
878
942
|
// --- word/styles.xml ---
|
|
879
943
|
const stylesXml = buildStylesXml(options, ctx);
|
|
880
944
|
pkg.addPart('word/styles.xml', stylesXml, CONTENT_TYPE_DOCX_STYLES);
|
|
@@ -942,9 +1006,31 @@ async function buildDocxPackage(
|
|
|
942
1006
|
});
|
|
943
1007
|
}
|
|
944
1008
|
|
|
1009
|
+
if (hasHeader) {
|
|
1010
|
+
pkg.addRelationship('word/document.xml', {
|
|
1011
|
+
id: headerRelId,
|
|
1012
|
+
type: REL_HEADER,
|
|
1013
|
+
target: 'header1.xml',
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (hasFooter) {
|
|
1018
|
+
pkg.addRelationship('word/document.xml', {
|
|
1019
|
+
id: footerRelId,
|
|
1020
|
+
type: REL_FOOTER,
|
|
1021
|
+
target: 'footer1.xml',
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
945
1025
|
// --- Dynamic relationships (hyperlinks, images) ---
|
|
946
1026
|
for (const rel of ctx.relationships) {
|
|
947
|
-
|
|
1027
|
+
const sourcePart =
|
|
1028
|
+
rel.source === 'header'
|
|
1029
|
+
? 'word/header1.xml'
|
|
1030
|
+
: rel.source === 'footer'
|
|
1031
|
+
? 'word/footer1.xml'
|
|
1032
|
+
: 'word/document.xml';
|
|
1033
|
+
pkg.addRelationship(sourcePart, {
|
|
948
1034
|
id: rel.id,
|
|
949
1035
|
type: rel.type,
|
|
950
1036
|
target: rel.target,
|
|
@@ -975,11 +1061,19 @@ async function buildDocxPackage(
|
|
|
975
1061
|
// XML Part Generators
|
|
976
1062
|
// ============================================
|
|
977
1063
|
|
|
978
|
-
function buildDocumentXml(
|
|
1064
|
+
function buildDocumentXml(
|
|
1065
|
+
bodyXml: string,
|
|
1066
|
+
backgroundColor?: string,
|
|
1067
|
+
headerRelId?: string,
|
|
1068
|
+
footerRelId?: string,
|
|
1069
|
+
): string {
|
|
979
1070
|
// `<w:background>` must be the FIRST child of `<w:document>` per the
|
|
980
1071
|
// WordprocessingML schema. Word also requires `<w:displayBackgroundShape/>`
|
|
981
1072
|
// in settings.xml before the bg actually paints (see buildSettingsXml).
|
|
982
1073
|
const backgroundEl = backgroundColor ? `<w:background w:color="${backgroundColor}"/>` : '';
|
|
1074
|
+
const storyReferences =
|
|
1075
|
+
(headerRelId ? `<w:headerReference w:type="default" r:id="${headerRelId}"/>` : '') +
|
|
1076
|
+
(footerRelId ? `<w:footerReference w:type="default" r:id="${footerRelId}"/>` : '');
|
|
983
1077
|
return (
|
|
984
1078
|
xmlDeclaration() +
|
|
985
1079
|
`<w:document` +
|
|
@@ -997,6 +1091,7 @@ function buildDocumentXml(bodyXml: string, backgroundColor?: string): string {
|
|
|
997
1091
|
`<w:body>` +
|
|
998
1092
|
bodyXml +
|
|
999
1093
|
`<w:sectPr>` +
|
|
1094
|
+
storyReferences +
|
|
1000
1095
|
`<w:pgSz w:w="12240" w:h="15840"/>` +
|
|
1001
1096
|
`<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>` +
|
|
1002
1097
|
`<w:cols w:space="720"/>` +
|
|
@@ -1006,6 +1101,19 @@ function buildDocumentXml(bodyXml: string, backgroundColor?: string): string {
|
|
|
1006
1101
|
);
|
|
1007
1102
|
}
|
|
1008
1103
|
|
|
1104
|
+
function buildStoryXml(kind: 'header' | 'footer', bodyXml: string): string {
|
|
1105
|
+
const element = kind === 'header' ? 'hdr' : 'ftr';
|
|
1106
|
+
return (
|
|
1107
|
+
xmlDeclaration() +
|
|
1108
|
+
`<w:${element}` +
|
|
1109
|
+
` xmlns:w="${NS_WML}"` +
|
|
1110
|
+
` xmlns:r="${NS_R}"` +
|
|
1111
|
+
` xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing">` +
|
|
1112
|
+
bodyXml +
|
|
1113
|
+
`</w:${element}>`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1009
1117
|
function buildStylesXml(options: DocxExportOptions, ctx: ExportContext): string {
|
|
1010
1118
|
const font = ctx.font;
|
|
1011
1119
|
const headingFont = ctx.headingFont;
|