@bendyline/squisq-formats 1.1.0 → 1.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/README.md +23 -10
- package/dist/{chunk-ULLIPBEJ.js → chunk-743COJWQ.js} +6 -2
- package/dist/chunk-743COJWQ.js.map +1 -0
- package/dist/{chunk-KAK4V57E.js → chunk-BHHEDPRB.js} +16 -112
- package/dist/chunk-BHHEDPRB.js.map +1 -0
- package/dist/{chunk-MQHCXI56.js → chunk-CLGUOVYR.js} +193 -23
- package/dist/chunk-CLGUOVYR.js.map +1 -0
- package/dist/chunk-FIFCQN3W.js +630 -0
- package/dist/chunk-FIFCQN3W.js.map +1 -0
- package/dist/chunk-KJ4NS4DX.js +120 -0
- package/dist/chunk-KJ4NS4DX.js.map +1 -0
- package/dist/{chunk-TBPD5PCU.js → chunk-S3Y7H2BK.js} +161 -11
- package/dist/chunk-S3Y7H2BK.js.map +1 -0
- package/dist/docx/export.d.ts +15 -0
- package/dist/docx/export.d.ts.map +1 -1
- package/dist/docx/export.js +144 -19
- package/dist/docx/export.js.map +1 -1
- package/dist/docx/import.d.ts +13 -0
- package/dist/docx/import.d.ts.map +1 -1
- package/dist/docx/import.js +102 -10
- package/dist/docx/import.js.map +1 -1
- package/dist/docx/index.d.ts +1 -1
- package/dist/docx/index.d.ts.map +1 -1
- package/dist/docx/index.js +1 -1
- package/dist/docx/index.js.map +1 -1
- package/dist/html/htmlTemplate.d.ts +6 -0
- package/dist/html/htmlTemplate.d.ts.map +1 -1
- package/dist/html/htmlTemplate.js +5 -1
- package/dist/html/htmlTemplate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/ooxml/index.d.ts +1 -1
- package/dist/ooxml/index.d.ts.map +1 -1
- package/dist/ooxml/index.js +1 -1
- package/dist/ooxml/index.js.map +1 -1
- package/dist/ooxml/namespaces.d.ts +10 -0
- package/dist/ooxml/namespaces.d.ts.map +1 -1
- package/dist/ooxml/namespaces.js +10 -0
- package/dist/ooxml/namespaces.js.map +1 -1
- package/dist/pdf/export.d.ts +6 -0
- package/dist/pdf/export.d.ts.map +1 -1
- package/dist/pdf/export.js +37 -9
- package/dist/pdf/export.js.map +1 -1
- package/dist/pdf/import.d.ts +16 -0
- package/dist/pdf/import.d.ts.map +1 -1
- package/dist/pdf/import.js +177 -0
- package/dist/pdf/import.js.map +1 -1
- package/dist/pdf/index.d.ts +1 -1
- package/dist/pdf/index.d.ts.map +1 -1
- package/dist/pdf/index.js +1 -1
- package/dist/pdf/index.js.map +1 -1
- package/dist/pptx/export.d.ts +75 -0
- package/dist/pptx/export.d.ts.map +1 -0
- package/dist/pptx/export.js +722 -0
- package/dist/pptx/export.js.map +1 -0
- package/dist/pptx/index.d.ts +12 -27
- package/dist/pptx/index.d.ts.map +1 -1
- package/dist/pptx/index.js +12 -22
- package/dist/pptx/index.js.map +1 -1
- package/dist/pptx/styles.d.ts +54 -0
- package/dist/pptx/styles.d.ts.map +1 -0
- package/dist/pptx/styles.js +73 -0
- package/dist/pptx/styles.js.map +1 -0
- package/dist/pptx/templates.d.ts +24 -0
- package/dist/pptx/templates.d.ts.map +1 -0
- package/dist/pptx/templates.js +158 -0
- package/dist/pptx/templates.js.map +1 -0
- package/package.json +2 -2
- package/src/docx/export.ts +183 -20
- package/src/docx/import.ts +127 -10
- package/src/docx/index.ts +1 -1
- package/src/html/htmlTemplate.ts +13 -0
- package/src/index.ts +2 -2
- package/src/ooxml/index.ts +7 -0
- package/src/ooxml/namespaces.ts +23 -0
- package/src/pdf/export.ts +57 -9
- package/src/pdf/import.ts +228 -0
- package/src/pdf/index.ts +1 -1
- package/src/pptx/export.ts +1041 -0
- package/src/pptx/index.ts +15 -37
- package/src/pptx/styles.ts +96 -0
- package/src/pptx/templates.ts +187 -0
- package/dist/chunk-532L4D5D.js +0 -21
- package/dist/chunk-532L4D5D.js.map +0 -1
- package/dist/chunk-KAK4V57E.js.map +0 -1
- package/dist/chunk-MQHCXI56.js.map +0 -1
- package/dist/chunk-TBPD5PCU.js.map +0 -1
- package/dist/chunk-ULLIPBEJ.js.map +0 -1
package/src/docx/export.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { Doc } from '@bendyline/squisq/schemas';
|
|
20
|
+
import type { Doc, Theme } from '@bendyline/squisq/schemas';
|
|
21
|
+
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
21
22
|
import { docToMarkdown } from '@bendyline/squisq/doc';
|
|
22
23
|
import type {
|
|
23
24
|
MarkdownDocument,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
REL_SETTINGS,
|
|
53
54
|
REL_FONT_TABLE,
|
|
54
55
|
REL_HYPERLINK,
|
|
56
|
+
REL_IMAGE,
|
|
55
57
|
REL_FOOTNOTES,
|
|
56
58
|
CONTENT_TYPE_DOCX_DOCUMENT,
|
|
57
59
|
CONTENT_TYPE_DOCX_STYLES,
|
|
@@ -90,6 +92,18 @@ export interface DocxExportOptions {
|
|
|
90
92
|
defaultFont?: string;
|
|
91
93
|
/** Default body font size in points. Default: 11 */
|
|
92
94
|
defaultFontSize?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Squisq theme ID to apply (e.g., 'documentary', 'cinematic').
|
|
97
|
+
* When set, overrides fonts with the theme's typography and applies
|
|
98
|
+
* the theme's primary color to headings.
|
|
99
|
+
*/
|
|
100
|
+
themeId?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Pre-resolved image data keyed by image URL/path as it appears in the
|
|
103
|
+
* markdown source. When provided, images are embedded in the .docx file
|
|
104
|
+
* as binary parts instead of emitting placeholder text.
|
|
105
|
+
*/
|
|
106
|
+
images?: Map<string, { data: ArrayBuffer | Uint8Array; contentType: string }>;
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
/**
|
|
@@ -167,13 +181,38 @@ class ExportContext {
|
|
|
167
181
|
hasFootnotes = false;
|
|
168
182
|
|
|
169
183
|
readonly font: string;
|
|
184
|
+
readonly headingFont: string;
|
|
170
185
|
readonly fontSize: number;
|
|
186
|
+
/** Heading text color (hex without #), or undefined for default */
|
|
187
|
+
readonly headingColor: string | undefined;
|
|
188
|
+
|
|
189
|
+
/** Pre-resolved image data keyed by markdown image URL */
|
|
190
|
+
readonly resolvedImages: Map<string, { data: ArrayBuffer | Uint8Array; contentType: string }>;
|
|
191
|
+
|
|
192
|
+
private nextDocPrId = 1;
|
|
171
193
|
|
|
172
194
|
constructor(options: DocxExportOptions) {
|
|
173
|
-
|
|
195
|
+
let themeFont: string | undefined;
|
|
196
|
+
let themeTitleFont: string | undefined;
|
|
197
|
+
let themeHeadingColor: string | undefined;
|
|
198
|
+
|
|
199
|
+
if (options.themeId) {
|
|
200
|
+
const theme: Theme = resolveTheme(options.themeId);
|
|
201
|
+
themeFont = theme.typography?.bodyFontFamily;
|
|
202
|
+
themeTitleFont = theme.typography?.titleFontFamily;
|
|
203
|
+
if (theme.colors?.primary) {
|
|
204
|
+
const c = theme.colors.primary;
|
|
205
|
+
themeHeadingColor = c.startsWith('#') ? c.slice(1) : c;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.font = options.defaultFont ?? themeFont ?? DEFAULT_FONT;
|
|
210
|
+
this.headingFont = themeTitleFont ?? this.font;
|
|
174
211
|
this.fontSize = options.defaultFontSize
|
|
175
212
|
? options.defaultFontSize * 2
|
|
176
213
|
: DEFAULT_FONT_SIZE_HALF_POINTS;
|
|
214
|
+
this.headingColor = themeHeadingColor;
|
|
215
|
+
this.resolvedImages = options.images ?? new Map();
|
|
177
216
|
}
|
|
178
217
|
|
|
179
218
|
/** Allocate a new relationship ID */
|
|
@@ -193,6 +232,26 @@ class ExportContext {
|
|
|
193
232
|
return id;
|
|
194
233
|
}
|
|
195
234
|
|
|
235
|
+
/** Add an embedded image and return the rId and docPrId */
|
|
236
|
+
addImage(
|
|
237
|
+
data: ArrayBuffer | Uint8Array,
|
|
238
|
+
contentType: string,
|
|
239
|
+
filename: string,
|
|
240
|
+
): { relId: string; docPrId: number } {
|
|
241
|
+
const relId = this.allocRelId();
|
|
242
|
+
const docPrId = this.nextDocPrId++;
|
|
243
|
+
const path = `word/media/${filename}`;
|
|
244
|
+
|
|
245
|
+
this.images.push({ relId, path, data, contentType });
|
|
246
|
+
this.relationships.push({
|
|
247
|
+
id: relId,
|
|
248
|
+
type: REL_IMAGE,
|
|
249
|
+
target: `media/${filename}`,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return { relId, docPrId };
|
|
253
|
+
}
|
|
254
|
+
|
|
196
255
|
/** Allocate a numbering definition for a list */
|
|
197
256
|
allocNumbering(ordered: boolean): number {
|
|
198
257
|
const numId = this.nextNumId++;
|
|
@@ -509,7 +568,7 @@ function convertInline(node: MarkdownInlineNode, ctx: ExportContext, format: Inl
|
|
|
509
568
|
case 'link':
|
|
510
569
|
return convertLink(node, ctx, format);
|
|
511
570
|
case 'image':
|
|
512
|
-
return convertImage(node);
|
|
571
|
+
return convertImage(node, ctx);
|
|
513
572
|
case 'break':
|
|
514
573
|
return `<w:r><w:br/></w:r>`;
|
|
515
574
|
case 'htmlInline':
|
|
@@ -546,10 +605,6 @@ function makeRun(text: string, format: InlineFormat): string {
|
|
|
546
605
|
|
|
547
606
|
function convertLink(node: MarkdownLink, ctx: ExportContext, format: InlineFormat): string {
|
|
548
607
|
const rId = ctx.addHyperlink(node.url);
|
|
549
|
-
const _runs = convertInlines(node.children, ctx, { ...format });
|
|
550
|
-
|
|
551
|
-
// Wrap each run's rPr with hyperlink styling
|
|
552
|
-
// For simplicity, emit inline runs with hyperlink color + underline
|
|
553
608
|
const styledRuns = convertInlinesWithHyperlinkStyle(node.children, ctx, format);
|
|
554
609
|
|
|
555
610
|
return `<w:hyperlink r:id="${rId}">${styledRuns}</w:hyperlink>`;
|
|
@@ -589,13 +644,119 @@ function makeHyperlinkRun(text: string, format: InlineFormat): string {
|
|
|
589
644
|
);
|
|
590
645
|
}
|
|
591
646
|
|
|
592
|
-
function convertImage(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
647
|
+
function convertImage(node: MarkdownImage, ctx: ExportContext): string {
|
|
648
|
+
const imageEntry = ctx.resolvedImages.get(node.url);
|
|
649
|
+
if (!imageEntry) {
|
|
650
|
+
// No resolved data — emit placeholder text
|
|
651
|
+
const alt = node.alt || node.url;
|
|
652
|
+
return makeRun(`[Image: ${alt}]`, { italic: true });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const { data, contentType } = imageEntry;
|
|
656
|
+
const ext = contentType.split('/')[1]?.replace('jpeg', 'jpg') || 'png';
|
|
657
|
+
const filename = `image${ctx.images.length + 1}.${ext}`;
|
|
658
|
+
const { relId, docPrId } = ctx.addImage(data, contentType, filename);
|
|
659
|
+
|
|
660
|
+
// Read dimensions from binary header; fall back to 5×3 inches
|
|
661
|
+
const dims = readImageDimensions(data);
|
|
662
|
+
const EMU_PER_INCH = 914400;
|
|
663
|
+
const MAX_WIDTH_EMU = 6 * EMU_PER_INCH; // 6 inch content width
|
|
664
|
+
let cx: number;
|
|
665
|
+
let cy: number;
|
|
666
|
+
|
|
667
|
+
if (dims) {
|
|
668
|
+
// Scale to fit within max width, assuming 96 DPI for pixel → inch
|
|
669
|
+
const widthEmu = (dims.width / 96) * EMU_PER_INCH;
|
|
670
|
+
const heightEmu = (dims.height / 96) * EMU_PER_INCH;
|
|
671
|
+
if (widthEmu > MAX_WIDTH_EMU) {
|
|
672
|
+
const scale = MAX_WIDTH_EMU / widthEmu;
|
|
673
|
+
cx = MAX_WIDTH_EMU;
|
|
674
|
+
cy = Math.round(heightEmu * scale);
|
|
675
|
+
} else {
|
|
676
|
+
cx = Math.round(widthEmu);
|
|
677
|
+
cy = Math.round(heightEmu);
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
cx = 5 * EMU_PER_INCH;
|
|
681
|
+
cy = 3 * EMU_PER_INCH;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const name = escapeXml(node.alt || filename);
|
|
685
|
+
const NS_A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
|
|
686
|
+
const NS_PIC = 'http://schemas.openxmlformats.org/drawingml/2006/picture';
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
`<w:r><w:drawing>` +
|
|
690
|
+
`<wp:inline distT="0" distB="0" distL="0" distR="0">` +
|
|
691
|
+
`<wp:extent cx="${cx}" cy="${cy}"/>` +
|
|
692
|
+
`<wp:docPr id="${docPrId}" name="${name}"/>` +
|
|
693
|
+
`<wp:cNvGraphicFramePr>` +
|
|
694
|
+
`<a:graphicFrameLocks xmlns:a="${NS_A}" noChangeAspect="1"/>` +
|
|
695
|
+
`</wp:cNvGraphicFramePr>` +
|
|
696
|
+
`<a:graphic xmlns:a="${NS_A}">` +
|
|
697
|
+
`<a:graphicData uri="${NS_PIC}">` +
|
|
698
|
+
`<pic:pic xmlns:pic="${NS_PIC}">` +
|
|
699
|
+
`<pic:nvPicPr>` +
|
|
700
|
+
`<pic:cNvPr id="0" name="${name}"/>` +
|
|
701
|
+
`<pic:cNvPicPr/>` +
|
|
702
|
+
`</pic:nvPicPr>` +
|
|
703
|
+
`<pic:blipFill>` +
|
|
704
|
+
`<a:blip r:embed="${relId}"/>` +
|
|
705
|
+
`<a:stretch><a:fillRect/></a:stretch>` +
|
|
706
|
+
`</pic:blipFill>` +
|
|
707
|
+
`<pic:spPr>` +
|
|
708
|
+
`<a:xfrm>` +
|
|
709
|
+
`<a:off x="0" y="0"/>` +
|
|
710
|
+
`<a:ext cx="${cx}" cy="${cy}"/>` +
|
|
711
|
+
`</a:xfrm>` +
|
|
712
|
+
`<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` +
|
|
713
|
+
`</pic:spPr>` +
|
|
714
|
+
`</pic:pic>` +
|
|
715
|
+
`</a:graphicData>` +
|
|
716
|
+
`</a:graphic>` +
|
|
717
|
+
`</wp:inline>` +
|
|
718
|
+
`</w:drawing></w:r>`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Read width/height from PNG or JPEG binary headers. */
|
|
723
|
+
function readImageDimensions(
|
|
724
|
+
data: ArrayBuffer | Uint8Array,
|
|
725
|
+
): { width: number; height: number } | null {
|
|
726
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
727
|
+
if (bytes.length < 24) return null;
|
|
728
|
+
|
|
729
|
+
// PNG: signature 0x89504E47, IHDR chunk at byte 16
|
|
730
|
+
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
|
731
|
+
const width = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
|
|
732
|
+
const height = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
|
|
733
|
+
return { width, height };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// JPEG: search for SOF0 (0xFFC0) or SOF2 (0xFFC2) marker
|
|
737
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
738
|
+
let offset = 2;
|
|
739
|
+
while (offset < bytes.length - 9) {
|
|
740
|
+
if (bytes[offset] !== 0xff) break;
|
|
741
|
+
const marker = bytes[offset + 1];
|
|
742
|
+
if (marker === 0xc0 || marker === 0xc2) {
|
|
743
|
+
const height = (bytes[offset + 5] << 8) | bytes[offset + 6];
|
|
744
|
+
const width = (bytes[offset + 7] << 8) | bytes[offset + 8];
|
|
745
|
+
return { width, height };
|
|
746
|
+
}
|
|
747
|
+
const segLen = (bytes[offset + 2] << 8) | bytes[offset + 3];
|
|
748
|
+
offset += 2 + segLen;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// GIF: width at bytes 6-7, height at bytes 8-9 (little-endian)
|
|
753
|
+
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
|
|
754
|
+
const width = bytes[6] | (bytes[7] << 8);
|
|
755
|
+
const height = bytes[8] | (bytes[9] << 8);
|
|
756
|
+
return { width, height };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return null;
|
|
599
760
|
}
|
|
600
761
|
|
|
601
762
|
function convertFootnoteRef(node: MarkdownFootnoteReference, ctx: ExportContext): string {
|
|
@@ -632,7 +793,7 @@ async function buildDocxPackage(
|
|
|
632
793
|
pkg.addPart('word/document.xml', documentXml, CONTENT_TYPE_DOCX_DOCUMENT);
|
|
633
794
|
|
|
634
795
|
// --- word/styles.xml ---
|
|
635
|
-
const stylesXml = buildStylesXml(options);
|
|
796
|
+
const stylesXml = buildStylesXml(options, ctx);
|
|
636
797
|
pkg.addPart('word/styles.xml', stylesXml, CONTENT_TYPE_DOCX_STYLES);
|
|
637
798
|
|
|
638
799
|
// --- word/settings.xml ---
|
|
@@ -754,9 +915,9 @@ function buildDocumentXml(bodyXml: string): string {
|
|
|
754
915
|
);
|
|
755
916
|
}
|
|
756
917
|
|
|
757
|
-
function buildStylesXml(options: DocxExportOptions): string {
|
|
758
|
-
const font =
|
|
759
|
-
const headingFont =
|
|
918
|
+
function buildStylesXml(options: DocxExportOptions, ctx: ExportContext): string {
|
|
919
|
+
const font = ctx.font;
|
|
920
|
+
const headingFont = ctx.headingFont;
|
|
760
921
|
|
|
761
922
|
return (
|
|
762
923
|
xmlDeclaration() +
|
|
@@ -776,7 +937,7 @@ function buildStylesXml(options: DocxExportOptions): string {
|
|
|
776
937
|
`<w:qFormat/>` +
|
|
777
938
|
`</w:style>` +
|
|
778
939
|
// Heading styles
|
|
779
|
-
buildHeadingStyles(headingFont) +
|
|
940
|
+
buildHeadingStyles(headingFont, ctx.headingColor) +
|
|
780
941
|
// Quote style
|
|
781
942
|
`<w:style w:type="paragraph" w:styleId="Quote">` +
|
|
782
943
|
`<w:name w:val="Quote"/>` +
|
|
@@ -827,11 +988,12 @@ function buildStylesXml(options: DocxExportOptions): string {
|
|
|
827
988
|
);
|
|
828
989
|
}
|
|
829
990
|
|
|
830
|
-
function buildHeadingStyles(headingFont: string): string {
|
|
991
|
+
function buildHeadingStyles(headingFont: string, headingColor?: string): string {
|
|
831
992
|
let result = '';
|
|
832
993
|
for (let depth = 1; depth <= 6; depth++) {
|
|
833
994
|
const styleId = DEPTH_TO_STYLE_ID[depth];
|
|
834
995
|
const fontSize = HEADING_FONT_SIZES[depth] ?? 22;
|
|
996
|
+
const colorXml = headingColor ? `<w:color w:val="${headingColor}"/>` : '';
|
|
835
997
|
result +=
|
|
836
998
|
`<w:style w:type="paragraph" w:styleId="${styleId}">` +
|
|
837
999
|
`<w:name w:val="heading ${depth}"/>` +
|
|
@@ -842,6 +1004,7 @@ function buildHeadingStyles(headingFont: string): string {
|
|
|
842
1004
|
`<w:rPr>` +
|
|
843
1005
|
`<w:rFonts w:ascii="${escapeXml(headingFont)}" w:hAnsi="${escapeXml(headingFont)}"/>` +
|
|
844
1006
|
`<w:b/>` +
|
|
1007
|
+
colorXml +
|
|
845
1008
|
`<w:sz w:val="${fontSize}"/>` +
|
|
846
1009
|
`<w:szCs w:val="${fontSize}"/>` +
|
|
847
1010
|
`</w:rPr>` +
|
package/src/docx/import.ts
CHANGED
|
@@ -46,9 +46,11 @@ import type {
|
|
|
46
46
|
MarkdownFootnoteDefinition,
|
|
47
47
|
} from '@bendyline/squisq/markdown';
|
|
48
48
|
|
|
49
|
-
import { openPackage, getPartXml, getPartRelationships } from '../ooxml/reader.js';
|
|
49
|
+
import { openPackage, getPartXml, getPartBinary, getPartRelationships } from '../ooxml/reader.js';
|
|
50
50
|
import type { OoxmlPackage, Relationship } from '../ooxml/types.js';
|
|
51
51
|
import { NS_WML, NS_R } from '../ooxml/namespaces.js';
|
|
52
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
53
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
52
54
|
import {
|
|
53
55
|
HEADING_STYLE_MAP,
|
|
54
56
|
QUOTE_STYLE_IDS,
|
|
@@ -119,6 +121,56 @@ export async function docxToDoc(
|
|
|
119
121
|
return markdownToDoc(markdownDoc);
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Convert a .docx file to a ContentContainer with markdown + extracted images.
|
|
126
|
+
*
|
|
127
|
+
* The container will contain:
|
|
128
|
+
* - The primary markdown document (index.md)
|
|
129
|
+
* - Any embedded images under images/ (e.g., images/image1.png)
|
|
130
|
+
*
|
|
131
|
+
* @param data - The raw .docx file as ArrayBuffer or Blob
|
|
132
|
+
* @param options - Import options
|
|
133
|
+
* @returns A ContentContainer with the document and its media
|
|
134
|
+
*/
|
|
135
|
+
export async function docxToContainer(
|
|
136
|
+
data: ArrayBuffer | Blob,
|
|
137
|
+
options: DocxImportOptions = {},
|
|
138
|
+
): Promise<ContentContainer> {
|
|
139
|
+
const pkg = await openPackage(data);
|
|
140
|
+
const ctx = await buildImportContext(pkg, { ...options, extractImages: true });
|
|
141
|
+
|
|
142
|
+
const documentXml = await getPartXml(pkg, 'word/document.xml');
|
|
143
|
+
if (!documentXml) {
|
|
144
|
+
const container = new MemoryContentContainer();
|
|
145
|
+
await container.writeDocument('');
|
|
146
|
+
return container;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const body = getFirstElement(documentXml, 'body');
|
|
150
|
+
if (!body) {
|
|
151
|
+
const container = new MemoryContentContainer();
|
|
152
|
+
await container.writeDocument('');
|
|
153
|
+
return container;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const blocks = await convertBody(body, ctx);
|
|
157
|
+
const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
|
|
158
|
+
|
|
159
|
+
// Serialize to markdown
|
|
160
|
+
const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
161
|
+
const markdown = stringifyMarkdown(markdownDoc);
|
|
162
|
+
|
|
163
|
+
// Build container with markdown + images
|
|
164
|
+
const container = new MemoryContentContainer();
|
|
165
|
+
await container.writeDocument(markdown);
|
|
166
|
+
|
|
167
|
+
for (const [path, { data: imageData, mimeType }] of ctx.extractedImages) {
|
|
168
|
+
await container.writeFile(path, new Uint8Array(imageData), mimeType);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return container;
|
|
172
|
+
}
|
|
173
|
+
|
|
122
174
|
// ============================================
|
|
123
175
|
// Import Context
|
|
124
176
|
// ============================================
|
|
@@ -142,6 +194,10 @@ interface ImportContext {
|
|
|
142
194
|
pkg: OoxmlPackage;
|
|
143
195
|
/** Import options */
|
|
144
196
|
options: DocxImportOptions;
|
|
197
|
+
/** Collected image files: relative path → { data, mimeType } */
|
|
198
|
+
extractedImages: Map<string, { data: ArrayBuffer; mimeType: string }>;
|
|
199
|
+
/** Counter for generating unique image filenames */
|
|
200
|
+
imageCounter: number;
|
|
145
201
|
}
|
|
146
202
|
|
|
147
203
|
interface NumberingInfo {
|
|
@@ -162,6 +218,8 @@ async function buildImportContext(
|
|
|
162
218
|
footnotes: new Map(),
|
|
163
219
|
pkg,
|
|
164
220
|
options,
|
|
221
|
+
extractedImages: new Map(),
|
|
222
|
+
imageCounter: 0,
|
|
165
223
|
};
|
|
166
224
|
|
|
167
225
|
// Initialize with built-in defaults
|
|
@@ -544,20 +602,79 @@ async function convertHyperlink(el: Element, ctx: ImportContext): Promise<Markdo
|
|
|
544
602
|
// Image Extraction
|
|
545
603
|
// ============================================
|
|
546
604
|
|
|
547
|
-
async function extractImage(
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
605
|
+
async function extractImage(el: Element, ctx: ImportContext): Promise<MarkdownImage | null> {
|
|
606
|
+
// Find <a:blip r:embed="rIdX"/> anywhere in the drawing tree
|
|
607
|
+
const blip = findDescendant(el, 'blip');
|
|
608
|
+
if (!blip) {
|
|
609
|
+
return { type: 'image', url: '', alt: 'Image' };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const rId = blip.getAttributeNS(NS_R, 'embed') ?? blip.getAttribute('r:embed');
|
|
613
|
+
if (!rId) {
|
|
614
|
+
return { type: 'image', url: '', alt: 'Image' };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const rel = ctx.documentRels.get(rId);
|
|
618
|
+
if (!rel) {
|
|
619
|
+
return { type: 'image', url: '', alt: 'Image' };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Resolve the target path relative to word/
|
|
623
|
+
const target = rel.target.startsWith('/') ? rel.target.slice(1) : `word/${rel.target}`;
|
|
624
|
+
|
|
625
|
+
// Extract binary data from the zip
|
|
626
|
+
const data = await getPartBinary(ctx.pkg, target);
|
|
627
|
+
if (!data) {
|
|
628
|
+
return { type: 'image', url: '', alt: 'Image' };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Determine extension and MIME type
|
|
632
|
+
const dot = target.lastIndexOf('.');
|
|
633
|
+
const ext = dot !== -1 ? target.slice(dot).toLowerCase() : '.png';
|
|
634
|
+
const mimeType = IMAGE_MIME_MAP[ext] ?? 'application/octet-stream';
|
|
635
|
+
|
|
636
|
+
// Generate a unique image path
|
|
637
|
+
ctx.imageCounter++;
|
|
638
|
+
const imagePath = `images/image${ctx.imageCounter}${ext}`;
|
|
639
|
+
|
|
640
|
+
// Store the extracted image data
|
|
641
|
+
ctx.extractedImages.set(imagePath, { data, mimeType });
|
|
642
|
+
|
|
643
|
+
// Try to extract alt text from the drawing's docPr element
|
|
644
|
+
const docPr = findDescendant(el, 'docPr');
|
|
645
|
+
const alt = docPr?.getAttribute('descr') || docPr?.getAttribute('title') || 'Image';
|
|
646
|
+
|
|
554
647
|
return {
|
|
555
648
|
type: 'image',
|
|
556
|
-
url:
|
|
557
|
-
alt
|
|
649
|
+
url: imagePath,
|
|
650
|
+
alt,
|
|
558
651
|
};
|
|
559
652
|
}
|
|
560
653
|
|
|
654
|
+
const IMAGE_MIME_MAP: Record<string, string> = {
|
|
655
|
+
'.png': 'image/png',
|
|
656
|
+
'.jpg': 'image/jpeg',
|
|
657
|
+
'.jpeg': 'image/jpeg',
|
|
658
|
+
'.gif': 'image/gif',
|
|
659
|
+
'.bmp': 'image/bmp',
|
|
660
|
+
'.tiff': 'image/tiff',
|
|
661
|
+
'.tif': 'image/tiff',
|
|
662
|
+
'.svg': 'image/svg+xml',
|
|
663
|
+
'.webp': 'image/webp',
|
|
664
|
+
'.emf': 'image/emf',
|
|
665
|
+
'.wmf': 'image/wmf',
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/** Recursively find the first descendant element with the given local name. */
|
|
669
|
+
function findDescendant(el: Element, localName: string): Element | null {
|
|
670
|
+
for (const child of Array.from(el.children)) {
|
|
671
|
+
if (child.localName === localName) return child;
|
|
672
|
+
const found = findDescendant(child, localName);
|
|
673
|
+
if (found) return found;
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
561
678
|
// ============================================
|
|
562
679
|
// List Collection
|
|
563
680
|
// ============================================
|
package/src/docx/index.ts
CHANGED
|
@@ -22,5 +22,5 @@ export { markdownDocToDocx, docToDocx } from './export.js';
|
|
|
22
22
|
export type { DocxExportOptions } from './export.js';
|
|
23
23
|
|
|
24
24
|
// Import
|
|
25
|
-
export { docxToMarkdownDoc, docxToDoc } from './import.js';
|
|
25
|
+
export { docxToMarkdownDoc, docxToDoc, docxToContainer } from './import.js';
|
|
26
26
|
export type { DocxImportOptions } from './import.js';
|
package/src/html/htmlTemplate.ts
CHANGED
|
@@ -41,6 +41,13 @@ export interface HtmlExportOptions {
|
|
|
41
41
|
|
|
42
42
|
/** Auto-play slideshow on load (default: false) */
|
|
43
43
|
autoPlay?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Squisq theme ID to apply (e.g., 'documentary', 'cinematic').
|
|
47
|
+
* When set, the theme is assigned to the Doc before rendering,
|
|
48
|
+
* so the SquisqPlayer renders with that theme's colors and typography.
|
|
49
|
+
*/
|
|
50
|
+
themeId?: string;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
// ── Image Path Collection ──────────────────────────────────────────
|
|
@@ -182,8 +189,14 @@ export function generateInlineHtml(doc: Doc, options: HtmlExportOptions): string
|
|
|
182
189
|
mode = 'slideshow',
|
|
183
190
|
title = 'Squisq Document',
|
|
184
191
|
autoPlay = false,
|
|
192
|
+
themeId,
|
|
185
193
|
} = options;
|
|
186
194
|
|
|
195
|
+
// Apply theme to doc if specified (the SquisqPlayer respects doc.themeId at render time)
|
|
196
|
+
if (themeId) {
|
|
197
|
+
doc = { ...doc, themeId };
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
// Build base64 image map
|
|
188
201
|
const imageMap: Record<string, string> = {};
|
|
189
202
|
if (images) {
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Supported formats:
|
|
8
8
|
* - **DOCX** — Microsoft Word (import + export) ✅
|
|
9
9
|
* - **PDF** — Portable Document Format (import + export) ✅
|
|
10
|
-
* - **PPTX** — Microsoft PowerPoint (planned)
|
|
10
|
+
* - **PPTX** — Microsoft PowerPoint (export ✅, import planned)
|
|
11
11
|
* - **XLSX** — Microsoft Excel (planned)
|
|
12
12
|
*
|
|
13
13
|
* All converters run in the browser — no server or native binaries required.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
export { markdownDocToDocx, docToDocx, docxToMarkdownDoc, docxToDoc } from './docx/index.js';
|
|
29
29
|
export type { DocxExportOptions, DocxImportOptions } from './docx/index.js';
|
|
30
30
|
|
|
31
|
-
// PPTX (stub)
|
|
31
|
+
// PPTX (export implemented, import stub)
|
|
32
32
|
export { markdownDocToPptx, docToPptx, pptxToMarkdownDoc, pptxToDoc } from './pptx/index.js';
|
|
33
33
|
export type { PptxExportOptions, PptxImportOptions } from './pptx/index.js';
|
|
34
34
|
|
package/src/ooxml/index.ts
CHANGED
|
@@ -83,5 +83,12 @@ export {
|
|
|
83
83
|
CONTENT_TYPE_DOCX_FONT_TABLE,
|
|
84
84
|
CONTENT_TYPE_DOCX_FOOTNOTES,
|
|
85
85
|
CONTENT_TYPE_PPTX_PRESENTATION,
|
|
86
|
+
CONTENT_TYPE_PPTX_SLIDE,
|
|
87
|
+
CONTENT_TYPE_PPTX_SLIDE_LAYOUT,
|
|
88
|
+
CONTENT_TYPE_PPTX_SLIDE_MASTER,
|
|
89
|
+
CONTENT_TYPE_PPTX_THEME,
|
|
90
|
+
REL_SLIDE,
|
|
91
|
+
REL_SLIDE_LAYOUT,
|
|
92
|
+
REL_SLIDE_MASTER,
|
|
86
93
|
CONTENT_TYPE_XLSX_WORKBOOK,
|
|
87
94
|
} from './namespaces.js';
|
package/src/ooxml/namespaces.ts
CHANGED
|
@@ -156,5 +156,28 @@ export const CONTENT_TYPE_DOCX_FOOTNOTES =
|
|
|
156
156
|
export const CONTENT_TYPE_PPTX_PRESENTATION =
|
|
157
157
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml';
|
|
158
158
|
|
|
159
|
+
export const CONTENT_TYPE_PPTX_SLIDE =
|
|
160
|
+
'application/vnd.openxmlformats-officedocument.presentationml.slide+xml';
|
|
161
|
+
|
|
162
|
+
export const CONTENT_TYPE_PPTX_SLIDE_LAYOUT =
|
|
163
|
+
'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml';
|
|
164
|
+
|
|
165
|
+
export const CONTENT_TYPE_PPTX_SLIDE_MASTER =
|
|
166
|
+
'application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml';
|
|
167
|
+
|
|
168
|
+
export const CONTENT_TYPE_PPTX_THEME = 'application/vnd.openxmlformats-officedocument.theme+xml';
|
|
169
|
+
|
|
170
|
+
/** Relationship type: Slide */
|
|
171
|
+
export const REL_SLIDE =
|
|
172
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide';
|
|
173
|
+
|
|
174
|
+
/** Relationship type: Slide layout */
|
|
175
|
+
export const REL_SLIDE_LAYOUT =
|
|
176
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout';
|
|
177
|
+
|
|
178
|
+
/** Relationship type: Slide master */
|
|
179
|
+
export const REL_SLIDE_MASTER =
|
|
180
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster';
|
|
181
|
+
|
|
159
182
|
export const CONTENT_TYPE_XLSX_WORKBOOK =
|
|
160
183
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml';
|