Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * .docx Import module for Atmosphere Docs.
3 *
4 * Uses mammoth.js to convert .docx files to HTML, then feeds the result
5 * into the TipTap editor.
6 */
7import type { Editor } from '@tiptap/core';
8import type { DocxConvertResult, DocxMessage } from './types.js';
9
10/**
11 * Convert a .docx ArrayBuffer to HTML using mammoth.js.
12 * Pure async function — testable without DOM.
13 */
14export async function convertDocxToHtml(arrayBuffer: ArrayBuffer): Promise<DocxConvertResult> {
15 const mammoth = await import('mammoth');
16
17 // mammoth accepts { arrayBuffer } in the browser and { buffer } in Node.js.
18 // Detect environment and pass the right input format.
19 const isNode = typeof process !== 'undefined' && process.versions?.node;
20 const input = isNode ? { buffer: Buffer.from(arrayBuffer) } : { arrayBuffer };
21
22 const options = {
23 styleMap: [
24 "p[style-name='Heading 1'] => h1:fresh",
25 "p[style-name='Heading 2'] => h2:fresh",
26 "p[style-name='Heading 3'] => h3:fresh",
27 "p[style-name='Heading 4'] => h4:fresh",
28 "p[style-name='Heading 5'] => h5:fresh",
29 "p[style-name='Heading 6'] => h6:fresh",
30 ],
31 };
32
33 const result = await mammoth.convertToHtml(input, options);
34 return {
35 html: result.value,
36 messages: (result.messages || []) as DocxMessage[],
37 };
38}
39
40/**
41 * Validate that the given ArrayBuffer looks like a valid .docx file.
42 * A .docx is a ZIP file, so it starts with the PK signature (0x504B0304).
43 */
44export function isValidDocx(arrayBuffer: ArrayBuffer): boolean {
45 if (!arrayBuffer || arrayBuffer.byteLength < 4) return false;
46 const view = new Uint8Array(arrayBuffer);
47 return view[0] === 0x50 && view[1] === 0x4B && view[2] === 0x03 && view[3] === 0x04;
48}
49
50/**
51 * Import a .docx File object into the TipTap editor.
52 * DOM-coupled entry point — not unit-testable.
53 */
54export async function importDocx(
55 file: File,
56 editor: Editor,
57 showToast: (message: string, duration: number) => void,
58): Promise<void> {
59 try {
60 const arrayBuffer = await file.arrayBuffer();
61
62 if (!isValidDocx(arrayBuffer)) {
63 showToast('Invalid .docx file — the file appears to be corrupt', 5000);
64 return;
65 }
66
67 const { html, messages } = await convertDocxToHtml(arrayBuffer);
68
69 if (!html || html.trim() === '') {
70 showToast('The .docx file appears to be empty', 3000);
71 return;
72 }
73
74 editor.commands.setContent(html);
75
76 const warnings = messages.filter((m: DocxMessage) => m.type === 'warning');
77 if (warnings.length > 0) {
78 showToast(`Imported "${file.name}" with ${warnings.length} warning(s)`, 4000);
79 } else {
80 showToast(`Imported "${file.name}" successfully`, 3000);
81 }
82 } catch (err) {
83 console.error('docx import error:', err);
84 showToast('Failed to import .docx file — it may be corrupt or unsupported', 5000);
85 }
86}