feat(ee): docx word export

This commit is contained in:
Philipinho
2026-06-19 13:10:55 +01:00
parent 2b56c09afc
commit ba3c83bc1d
19 changed files with 431 additions and 387 deletions
@@ -6,13 +6,21 @@ import {
Select, Select,
Switch, Switch,
Divider, Divider,
Tooltip,
Badge,
} from "@mantine/core"; } from "@mantine/core";
import { exportPage } from "@/features/page/services/page-service.ts"; import {
exportPage,
exportPageToDocx,
} from "@/features/page/services/page-service.ts";
import { useState } from "react"; import { useState } from "react";
import { ExportFormat } from "@/features/page/types/page.types.ts"; import { ExportFormat } from "@/features/page/types/page.types.ts";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { exportSpace } from "@/features/space/services/space-service"; import { exportSpace } from "@/features/space/services/space-service";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Feature } from "@/ee/features";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
interface ExportModalProps { interface ExportModalProps {
id: string; id: string;
@@ -32,17 +40,25 @@ export default function ExportModal({
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false); const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
const [isExporting, setIsExporting] = useState<boolean>(false); const [isExporting, setIsExporting] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const upgradeLabel = useUpgradeLabel();
const isDocx = format === ExportFormat.Docx;
const docxEntitled = useHasFeature(Feature.DOCX_EXPORT);
const blockedByLicense = isDocx && !docxEntitled;
const handleExport = async () => { const handleExport = async () => {
setIsExporting(true); setIsExporting(true);
try { try {
if (type === "page") { if (type === "page") {
await exportPage({ if (format === ExportFormat.Docx) {
pageId: id, await exportPageToDocx({ pageId: id });
format, } else {
includeChildren, await exportPage({
includeAttachments, pageId: id,
}); format,
includeChildren,
includeAttachments,
});
}
} }
if (type === "space") { if (type === "space") {
await exportSpace({ spaceId: id, format, includeAttachments }); await exportSpace({ spaceId: id, format, includeAttachments });
@@ -88,10 +104,15 @@ export default function ExportModal({
<div> <div>
<Text size="md">{t("Format")}</Text> <Text size="md">{t("Format")}</Text>
</div> </div>
<ExportFormatSelection format={format} onChange={handleChange} /> <ExportFormatSelection
format={format}
onChange={handleChange}
includeDocx={type === "page"}
docxEntitled={docxEntitled}
/>
</Group> </Group>
{type === "page" && ( {type === "page" && !isDocx && (
<> <>
<Divider my="sm" /> <Divider my="sm" />
@@ -143,7 +164,16 @@ export default function ExportModal({
<Button onClick={onClose} variant="default"> <Button onClick={onClose} variant="default">
{t("Cancel")} {t("Cancel")}
</Button> </Button>
<Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button> <Tooltip label={upgradeLabel} disabled={!blockedByLicense} withArrow>
<Button
onClick={handleExport}
loading={isExporting}
disabled={blockedByLicense}
data-disabled={blockedByLicense || undefined}
>
{t("Export")}
</Button>
</Tooltip>
</Group> </Group>
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>
@@ -154,23 +184,49 @@ export default function ExportModal({
interface ExportFormatSelection { interface ExportFormatSelection {
format: ExportFormat; format: ExportFormat;
onChange: (value: string) => void; onChange: (value: string) => void;
includeDocx?: boolean;
docxEntitled?: boolean;
} }
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) { function ExportFormatSelection({
format,
onChange,
includeDocx,
docxEntitled,
}: ExportFormatSelection) {
const { t } = useTranslation(); const { t } = useTranslation();
const data = [
{ value: "markdown", label: "Markdown" },
{ value: "html", label: "HTML" },
...(includeDocx
? [{ value: "docx", label: "Word (.docx)", disabled: !docxEntitled }]
: []),
];
return ( return (
<Select <Select
data={[ data={data}
{ value: "markdown", label: "Markdown" },
{ value: "html", label: "HTML" },
]}
defaultValue={format} defaultValue={format}
onChange={onChange} onChange={onChange}
styles={{ wrapper: { maxWidth: 120 } }} styles={{ wrapper: { maxWidth: 140 }, option: { opacity: 1 } }}
comboboxProps={{ width: "120" }} comboboxProps={{ width: 200 }}
allowDeselect={false} allowDeselect={false}
withCheckIcon={false} withCheckIcon={false}
aria-label={t("Select export format")} aria-label={t("Select export format")}
renderOption={({ option }) =>
option.value === "docx" && !docxEntitled ? (
<div>
<Text size="sm" c="dimmed">
{option.label}
</Text>
<Badge size="xs" mt={4}>
{t("Enterprise")}
</Badge>
</div>
) : (
<Text size="sm">{option.label}</Text>
)
}
/> />
); );
} }
+1
View File
@@ -19,4 +19,5 @@ export const Feature = {
SHARING_CONTROLS: 'sharing:controls', SHARING_CONTROLS: 'sharing:controls',
TEMPLATES: 'templates', TEMPLATES: 'templates',
VIEWER_COMMENTS: 'comment:viewer', VIEWER_COMMENTS: 'comment:viewer',
DOCX_EXPORT: 'export:docx',
} as const; } as const;
@@ -132,6 +132,25 @@ export async function exportPage(data: IExportPageParams): Promise<void> {
saveAs(req.data, decodedFileName); saveAs(req.data, decodedFileName);
} }
export async function exportPageToDocx(data: { pageId: string }): Promise<void> {
const req = await api.post("/docx-export", data, {
responseType: "blob",
});
const fileName = req?.headers["content-disposition"]
.split("filename=")[1]
.replace(/"/g, "");
let decodedFileName = fileName;
try {
decodedFileName = decodeURIComponent(fileName);
} catch (err) {
// fallback to raw filename
}
saveAs(req.data, decodedFileName);
}
export async function importPage(file: File, spaceId: string) { export async function importPage(file: File, spaceId: string) {
const formData = new FormData(); const formData = new FormData();
formData.append("spaceId", spaceId); formData.append("spaceId", spaceId);
@@ -98,4 +98,5 @@ export interface IExportPageParams {
export enum ExportFormat { export enum ExportFormat {
HTML = "html", HTML = "html",
Markdown = "markdown", Markdown = "markdown",
Docx = "docx",
} }
+5 -1
View File
@@ -10,7 +10,11 @@ const api: AxiosInstance = axios.create({
api.interceptors.response.use( api.interceptors.response.use(
(response) => { (response) => {
// we need the response headers for these endpoints // we need the response headers for these endpoints
const exemptEndpoints = ["/api/pages/export", "/api/spaces/export"]; const exemptEndpoints = [
"/api/pages/export",
"/api/spaces/export",
"/api/docx-export",
];
if (response.request.responseURL) { if (response.request.responseURL) {
const path = new URL(response.request.responseURL)?.pathname; const path = new URL(response.request.responseURL)?.pathname;
if (path && exemptEndpoints.includes(path)) { if (path && exemptEndpoints.includes(path)) {
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2021",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2021", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
+21 -20
View File
@@ -30,14 +30,14 @@
"test:e2e": "jest --config test/jest-e2e.json" "test:e2e": "jest --config test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/google": "^3.0.52", "@ai-sdk/google": "3.0.52",
"@ai-sdk/openai": "^3.0.47", "@ai-sdk/openai": "3.0.47",
"@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/openai-compatible": "2.0.37",
"@aws-sdk/client-s3": "3.1050.0", "@aws-sdk/client-s3": "3.1050.0",
"@aws-sdk/lib-storage": "3.1050.0", "@aws-sdk/lib-storage": "3.1050.0",
"@aws-sdk/s3-request-presigner": "3.1050.0", "@aws-sdk/s3-request-presigner": "3.1050.0",
"@azure/storage-blob": "12.31.0", "@azure/storage-blob": "12.31.0",
"@clickhouse/client": "^1.18.2", "@clickhouse/client": "1.18.2",
"@docmost/pdf-inspector": "1.9.6", "@docmost/pdf-inspector": "1.9.6",
"@fastify/cookie": "^11.0.2", "@fastify/cookie": "^11.0.2",
"@fastify/multipart": "^10.0.0", "@fastify/multipart": "^10.0.0",
@@ -65,19 +65,19 @@
"@nestjs/websockets": "^11.1.19", "@nestjs/websockets": "^11.1.19",
"@node-saml/passport-saml": "^5.1.0", "@node-saml/passport-saml": "^5.1.0",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"ai": "^6.0.134", "ai": "6.0.134",
"ai-sdk-ollama": "^3.8.1", "ai-sdk-ollama": "3.8.1",
"bcrypt": "^6.0.0", "bcrypt": "6.0.0",
"bowser": "^2.14.1", "bowser": "2.14.1",
"bullmq": "^5.76.10", "bullmq": "5.76.10",
"cache-manager": "^7.2.8", "cache-manager": "7.2.8",
"cheerio": "^1.2.0", "cheerio": "1.2.0",
"class-transformer": "^0.5.1", "class-transformer": "0.5.1",
"class-validator": "^0.15.1", "class-validator": "0.15.1",
"cookie": "^1.1.1", "cookie": "1.1.1",
"fast-bm25": "0.0.5", "fast-bm25": "0.0.5",
"fastify-ip": "^2.0.0", "fastify-ip": "2.0.0",
"fs-extra": "^11.3.4", "fs-extra": "11.3.4",
"happy-dom": "20.8.9", "happy-dom": "20.8.9",
"ioredis": "^5.10.1", "ioredis": "^5.10.1",
"js-tiktoken": "^1.0.21", "js-tiktoken": "^1.0.21",
@@ -114,9 +114,9 @@
"scimmy": "1.3.5", "scimmy": "1.3.5",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"stripe": "^17.7.0", "stripe": "^17.7.0",
"tlds": "^1.261.0", "tlds": "1.261.0",
"tmp-promise": "^3.0.3", "tmp-promise": "3.0.3",
"tseep": "^1.3.1", "tseep": "1.3.1",
"typesense": "^3.0.5", "typesense": "^3.0.5",
"undici": "7.24.0", "undici": "7.24.0",
"ws": "^8.20.1", "ws": "^8.20.1",
@@ -192,7 +192,8 @@
"moduleNameMapper": { "moduleNameMapper": {
"^@docmost/db/(.*)$": "<rootDir>/database/$1", "^@docmost/db/(.*)$": "<rootDir>/database/$1",
"^@docmost/transactional/(.*)$": "<rootDir>/integrations/transactional/$1", "^@docmost/transactional/(.*)$": "<rootDir>/integrations/transactional/$1",
"^@docmost/ee/(.*)$": "<rootDir>/ee/$1" "^@docmost/ee/(.*)$": "<rootDir>/ee/$1",
"^src/(.*)$": "<rootDir>/$1"
} }
} }
} }
+1
View File
@@ -20,6 +20,7 @@ export const Feature = {
VIEWER_COMMENTS: 'comment:viewer', VIEWER_COMMENTS: 'comment:viewer',
TEMPLATES: 'templates', TEMPLATES: 'templates',
PDF_EXPORT: 'export:pdf', PDF_EXPORT: 'export:pdf',
DOCX_EXPORT: 'export:docx',
} as const; } as const;
export type FeatureKey = (typeof Feature)[keyof typeof Feature]; export type FeatureKey = (typeof Feature)[keyof typeof Feature];
+22 -21
View File
@@ -19,15 +19,15 @@
"clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite" "clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite"
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^7.1.2", "@braintree/sanitize-url": "7.1.2",
"@casl/ability": "6.8.0", "@casl/ability": "6.8.0",
"@docmost/editor-ext": "workspace:*", "@docmost/editor-ext": "workspace:*",
"@floating-ui/dom": "^1.7.3", "@floating-ui/dom": "1.7.3",
"@hocuspocus/provider": "3.4.4", "@hocuspocus/provider": "3.4.4",
"@hocuspocus/server": "3.4.4", "@hocuspocus/server": "3.4.4",
"@hocuspocus/transformer": "3.4.4", "@hocuspocus/transformer": "3.4.4",
"@joplin/turndown": "^4.0.82", "@joplin/turndown": "4.0.82",
"@joplin/turndown-plugin-gfm": "^1.0.64", "@joplin/turndown-plugin-gfm": "1.0.64",
"@sindresorhus/slugify": "3.0.0", "@sindresorhus/slugify": "3.0.0",
"@tiptap/core": "3.20.4", "@tiptap/core": "3.20.4",
"@tiptap/extension-audio": "3.20.4", "@tiptap/extension-audio": "3.20.4",
@@ -58,31 +58,32 @@
"@tiptap/starter-kit": "3.20.4", "@tiptap/starter-kit": "3.20.4",
"@tiptap/suggestion": "3.20.4", "@tiptap/suggestion": "3.20.4",
"@tiptap/y-tiptap": "3.0.2", "@tiptap/y-tiptap": "3.0.2",
"bytes": "^3.1.2", "bytes": "3.1.2",
"cross-env": "^10.1.0", "cross-env": "10.1.0",
"date-fns": "^4.1.0", "date-fns": "4.1.0",
"diff": "8.0.3", "diff": "8.0.3",
"docx": "9.7.1",
"dompurify": "3.4.1", "dompurify": "3.4.1",
"fractional-indexing-jittered": "^1.0.0", "fractional-indexing-jittered": "1.0.0",
"highlight.js": "^11.11.1", "highlight.js": "11.11.1",
"image-dimensions": "^2.5.0", "image-dimensions": "2.5.0",
"jszip": "^3.10.1", "jszip": "3.10.1",
"linkifyjs": "^4.3.2", "linkifyjs": "4.3.2",
"marked": "17.0.5", "marked": "17.0.5",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"qrcode": "^1.5.4", "qrcode": "1.5.4",
"rfc6902": "5.2.0", "rfc6902": "5.2.0",
"uuid": "^14.0.0", "uuid": "14.0.0",
"y-indexeddb": "^9.0.12", "y-indexeddb": "9.0.12",
"y-prosemirror": "1.3.7", "y-prosemirror": "1.3.7",
"yjs": "^13.6.30" "yjs": "^13.6.30"
}, },
"devDependencies": { "devDependencies": {
"@nx/js": "22.6.1", "@nx/js": "22.6.1",
"@types/bytes": "^3.1.5", "@types/bytes": "3.1.5",
"@types/qrcode": "^1.5.6", "@types/qrcode": "1.5.6",
"@types/turndown": "^5.0.6", "@types/turndown": "5.0.6",
"concurrently": "^9.2.1", "concurrently": "9.2.1",
"nx": "22.6.1", "nx": "22.6.1",
"tsx": "^4.21.0" "tsx": "^4.21.0"
}, },
@@ -133,8 +134,8 @@
"axios": "1.16.0", "axios": "1.16.0",
"langsmith": "0.7.0", "langsmith": "0.7.0",
"follow-redirects": "1.16.0", "follow-redirects": "1.16.0",
"protobufjs": "7.5.8", "protobufjs": "7.5.8",
"ip-address": "10.1.1" "ip-address": "10.1.1"
}, },
"neverBuiltDependencies": [] "neverBuiltDependencies": []
} }
+1
View File
@@ -2,6 +2,7 @@
"name": "@docmost/editor-ext", "name": "@docmost/editor-ext",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"private": true, "private": true,
"sideEffects": false,
"scripts": { "scripts": {
"build": "tsc --build", "build": "tsc --build",
"dev": "tsc --watch" "dev": "tsc --watch"
+4
View File
@@ -34,3 +34,7 @@ export * from "./lib/pdf";
export * from "./lib/page-break"; export * from "./lib/page-break";
export * from "./lib/resizable-nodeview"; export * from "./lib/resizable-nodeview";
export {
pageNodeToDocxBuffer,
type DocxImageResolver,
} from "./lib/prosemirror-docx";
@@ -16,10 +16,9 @@ export {
MAX_IMAGE_WIDTH, MAX_IMAGE_WIDTH,
} from './serializer'; } from './serializer';
export { export {
defaultDocxSerializer,
defaultDocxSerializerAsync,
defaultAsyncNodes, defaultAsyncNodes,
defaultNodes,
defaultMarks, defaultMarks,
pageNodeToDocxBuffer,
type DocxImageResolver,
} from './schema'; } from './schema';
export { writeDocx, createDocFromState, buildDoc } from './utils'; export { writeDocx, createDocFromState, buildDoc } from './utils';
@@ -1,76 +1,67 @@
import { HeadingLevel, ShadingType } from 'docx'; import { HeadingLevel, ShadingType } from 'docx';
import { Node } from 'prosemirror-model';
import { import {
DocxSerializer,
MarkSerializer,
NodeSerializer,
DocxSerializerAsync, DocxSerializerAsync,
MarkSerializer,
NodeSerializerAsync, NodeSerializerAsync,
OptionsAsync,
} from './serializer'; } from './serializer';
import { getLatexFromNode } from './utils'; import { writeDocx } from './utils';
export const defaultNodes: NodeSerializer = { export type DocxImageResolver = OptionsAsync['getImageBuffer'];
text(state, node) {
state.text(node.text ?? ''); // docx requires a 6-digit hex color (no leading #). Convert #rgb, #rrggbb,
}, // and rgb()/rgba() inputs to 6-digit hex; return undefined for anything else
paragraph(state, node) { // (named colors, hsl, etc.) so the caller omits the color rather than letting
state.renderInline(node); // docx throw "Invalid hex value".
state.closeBlock(node); function toDocxColor(input?: string): string | undefined {
}, if (!input) return undefined;
heading(state, node) { const value = input.trim().toLowerCase();
state.renderInline(node); const hex = value.startsWith('#') ? value.slice(1) : value;
const heading = [ if (/^[0-9a-f]{6}$/.test(hex)) return hex;
HeadingLevel.HEADING_1, if (/^[0-9a-f]{3}$/.test(hex)) {
HeadingLevel.HEADING_2, return hex
HeadingLevel.HEADING_3, .split('')
HeadingLevel.HEADING_4, .map((ch) => ch + ch)
HeadingLevel.HEADING_5, .join('');
HeadingLevel.HEADING_6, }
][node.attrs.level - 1]; const rgb = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
state.closeBlock(node, { heading }); if (rgb) {
}, const channel = (n: string) =>
blockquote(state, node) { Math.max(0, Math.min(255, parseInt(n, 10)))
state.renderContent(node, { style: 'IntenseQuote' }); .toString(16)
}, .padStart(2, '0');
code_block(state, node) { return channel(rgb[1]) + channel(rgb[2]) + channel(rgb[3]);
// TODO: something for code }
state.renderContent(node); return undefined;
state.closeBlock(node); }
},
horizontal_rule(state, node) { // Images and diagrams embed via the image resolver; the URL (with its file
// Kinda hacky, but this works to insert two paragraphs, the first with a break // extension) is passed through so docx can infer the image type.
state.closeBlock(node, { thematicBreak: true }); const renderImage: NodeSerializerAsync[string] = async (state, node) => {
state.closeBlock(node); const src = node.attrs?.src || node.attrs?.attachmentId;
}, if (src) {
hard_break(state) { try {
state.addRunOptions({ break: 1 }); await state.image(src, 100);
}, } catch {
ordered_list(state, node) { // Unrenderable/missing image: skip rather than fail the whole export.
state.renderList(node, 'numbered'); }
}, }
bullet_list(state, node) { state.closeBlock(node);
state.renderList(node, 'bullets'); };
},
list_item(state, node) { // Non-embeddable media render as a labelled line.
state.renderListItem(node); const renderFileLine: NodeSerializerAsync[string] = (state, node) => {
}, const label =
// Presentational node.attrs?.name || node.attrs?.src || node.attrs?.url || 'attachment';
image(state, node) { state.text(label);
const { src } = node.attrs; state.closeBlock(node);
state.image(src); };
state.closeBlock(node);
}, const renderEmbedLine: NodeSerializerAsync[string] = (state, node) => {
// Technical const label = node.attrs?.src || node.attrs?.url || 'embed';
math(state, node) { state.text(label);
state.math(getLatexFromNode(node), { inline: true }); state.closeBlock(node);
},
equation(state, node) {
const { id, numbered } = node.attrs;
state.math(getLatexFromNode(node), { inline: false, numbered, id });
state.closeBlock(node);
},
table(state, node) {
state.table(node);
},
}; };
export const defaultAsyncNodes: NodeSerializerAsync = { export const defaultAsyncNodes: NodeSerializerAsync = {
@@ -90,111 +81,170 @@ export const defaultAsyncNodes: NodeSerializerAsync = {
HeadingLevel.HEADING_4, HeadingLevel.HEADING_4,
HeadingLevel.HEADING_5, HeadingLevel.HEADING_5,
HeadingLevel.HEADING_6, HeadingLevel.HEADING_6,
][node.attrs.level - 1]; ][(node.attrs.level ?? 1) - 1];
state.closeBlock(node, { heading }); state.closeBlock(node, { heading });
}, },
blockquote(state, node) { async blockquote(state, node) {
state.renderContent(node, { style: 'IntenseQuote' }); await state.renderContent(node, { style: 'IntenseQuote' });
}, },
code_block(state, node) { async codeBlock(state, node) {
// TODO: something for code await state.renderContent(node);
state.renderContent(node);
state.closeBlock(node); state.closeBlock(node);
}, },
horizontal_rule(state, node) { horizontalRule(state, node) {
// Kinda hacky, but this works to insert two paragraphs, the first with a break
state.closeBlock(node, { thematicBreak: true }); state.closeBlock(node, { thematicBreak: true });
state.closeBlock(node); state.closeBlock(node);
}, },
hard_break(state) { hardBreak(state) {
state.addRunOptions({ break: 1 }); state.addRunOptions({ break: 1 });
}, },
async ordered_list(state, node) { async bulletList(state, node) {
await state.renderList(node, 'numbered');
},
async bullet_list(state, node) {
await state.renderList(node, 'bullets'); await state.renderList(node, 'bullets');
}, },
async list_item(state, node) { async orderedList(state, node) {
await state.renderList(node, 'numbered');
},
async listItem(state, node) {
await state.renderListItem(node); await state.renderListItem(node);
}, },
// Presentational async taskList(state, node) {
async image(state, node) { await state.renderList(node, 'bullets');
const { src } = node.attrs;
await state.image(src);
state.closeBlock(node);
}, },
// Technical async taskItem(state, node) {
math(state, node) { if (state.currentNumbering) {
state.math(getLatexFromNode(node), { inline: true }); state.addParagraphOptions({ numbering: state.currentNumbering });
}, }
equation(state, node) { state.text(node.attrs?.checked ? '☑ ' : '☐ ');
const { id, numbered } = node.attrs; await state.renderContent(node);
state.math(getLatexFromNode(node), { inline: false, numbered, id });
state.closeBlock(node);
}, },
async table(state, node) { async table(state, node) {
await state.table(node); await state.table(node);
}, },
// Docmost stores LaTeX in attrs.text.
mathInline(state, node) {
state.math(node.attrs?.text ?? '', { inline: true });
},
mathBlock(state, node) {
state.math(node.attrs?.text ?? '', { inline: false, numbered: false });
state.closeBlock(node);
},
image: renderImage,
drawio: renderImage,
excalidraw: renderImage,
video: renderFileLine,
audio: renderFileLine,
pdf: renderFileLine,
attachment: renderFileLine,
embed: renderEmbedLine,
youtube: renderEmbedLine,
async callout(state, node) {
await state.renderContent(node, { style: 'IntenseQuote' });
},
async details(state, node) {
await state.renderContent(node);
},
async detailsSummary(state, node) {
await state.renderInline(node);
state.closeBlock(node, { heading: HeadingLevel.HEADING_4 });
},
async detailsContent(state, node) {
await state.renderContent(node);
},
async columns(state, node) {
await state.renderContent(node);
},
async column(state, node) {
await state.renderContent(node);
},
async transclusionSource(state, node) {
await state.renderContent(node);
},
mention(state, node) {
state.text(`@${node.attrs?.label ?? ''}`);
},
status(state, node) {
state.text(`[${node.attrs?.text ?? ''}]`);
},
pageBreak(state, node) {
state.closeBlock(node, { pageBreakBefore: true });
},
// No usable static export representation: skip without failing.
subpages() {},
transclusionReference() {},
}; };
export const defaultMarks: MarkSerializer = { export const defaultMarks: MarkSerializer = {
em() { bold() {
return { italics: true };
},
strong() {
return { bold: true }; return { bold: true };
}, },
italic() { italic() {
return { italics: true }; return { italics: true };
}, },
bold() { strike() {
return { bold: true }; return { strike: true };
}, },
link() { underline() {
// Note, this is handled specifically in the serializer return { underline: {} };
// Word treats links more like a Node rather than a mark
return {};
}, },
code() { code() {
return { return {
font: { font: { name: 'Monospace' },
name: 'Monospace',
},
color: '000000', color: '000000',
shading: { shading: { type: ShadingType.SOLID, color: 'D2D3D2', fill: 'D2D3D2' },
type: ShadingType.SOLID,
color: 'D2D3D2',
fill: 'D2D3D2',
},
}; };
}, },
abbr() {
// TODO: abbreviation
return {};
},
subscript() {
return { subScript: true };
},
superscript() { superscript() {
return { superScript: true }; return { superScript: true };
}, },
strikethrough() { subscript() {
// doubleStrike! return { subScript: true };
return { strike: true };
}, },
underline() { link() {
return { // Handled specifically in the serializer; Word treats links as nodes.
underline: {}, return {};
};
}, },
smallcaps() { highlight(_state, _node, mark) {
return { smallCaps: true }; const fill = toDocxColor(mark.attrs?.color);
return fill
? { shading: { type: ShadingType.CLEAR, fill } }
: { highlight: 'yellow' };
}, },
allcaps() { // @tiptap/extension-color stores the color on the textStyle mark.
return { allCaps: true }; textStyle(_state, _node, mark) {
const color = toDocxColor(mark.attrs?.color);
return color ? { color } : {};
},
// Comments are editor-only; drop the annotation in the export.
comment() {
return {};
}, },
}; };
export const defaultDocxSerializer = new DocxSerializer(defaultNodes, defaultMarks); export async function pageNodeToDocxBuffer(
export const defaultDocxSerializerAsync = new DocxSerializerAsync(defaultAsyncNodes, defaultMarks); doc: Node,
getImageBuffer: DocxImageResolver,
): Promise<Buffer> {
const serializer = new DocxSerializerAsync(defaultAsyncNodes, defaultMarks);
const wordDoc = await serializer.serializeAsync(
doc,
{ getImageBuffer },
// docx's built-in heading styles are blue (#2E74B5 / #1F4D78). The editor
// has no heading color, so override the default heading run colors to the
// normal text color. Sizes/italics mirror docx's own defaults so only the
// color changes.
() =>
({
styles: {
default: {
heading1: { run: { color: '000000', size: 32 } },
heading2: { run: { color: '000000', size: 26 } },
heading3: { run: { color: '000000', size: 24 } },
heading4: { run: { color: '000000', italics: true } },
heading5: { run: { color: '000000' } },
heading6: { run: { color: '000000' } },
},
},
}) as any,
);
return writeDocx(wordDoc);
}
@@ -117,7 +117,7 @@ export class DocxSerializerState {
constructor(nodes: NodeSerializer, marks: MarkSerializer, options: Options) { constructor(nodes: NodeSerializer, marks: MarkSerializer, options: Options) {
this.nodes = nodes; this.nodes = nodes;
this.marks = marks; this.marks = marks;
this.options = options ?? {}; this.options = options ?? ({} as Options);
this.children = []; this.children = [];
this.numbering = []; this.numbering = [];
@@ -342,7 +342,7 @@ export class DocxSerializerState {
// Check if all cells are headers in this row // Check if all cells are headers in this row
let tableHeader = true; let tableHeader = true;
row.content.forEach((cell) => { row.content.forEach((cell) => {
if (cell.type.name !== 'table_header') { if (cell.type.name !== 'tableHeader') {
tableHeader = false; tableHeader = false;
} }
}); });
@@ -529,7 +529,7 @@ export class DocxSerializerStateAsync {
constructor(nodes: NodeSerializerAsync, marks: MarkSerializer, options: OptionsAsync) { constructor(nodes: NodeSerializerAsync, marks: MarkSerializer, options: OptionsAsync) {
this.nodes = nodes; this.nodes = nodes;
this.marks = marks; this.marks = marks;
this.options = options ?? {}; this.options = options ?? ({} as OptionsAsync);
this.children = []; this.children = [];
this.numbering = []; this.numbering = [];
@@ -765,7 +765,7 @@ export class DocxSerializerStateAsync {
// Check if all cells in the row are headers // Check if all cells in the row are headers
for (let cellIndex = 0; cellIndex < row.content.childCount; cellIndex += 1) { for (let cellIndex = 0; cellIndex < row.content.childCount; cellIndex += 1) {
const cell = row.content.child(cellIndex); const cell = row.content.child(cellIndex);
if (cell.type.name !== 'table_header') { if (cell.type.name !== 'tableHeader') {
tableHeader = false; tableHeader = false;
} }
} }
@@ -1,28 +0,0 @@
import { Schema } from 'prosemirror-model';
import { builders } from 'prosemirror-test-builder';
import { schemas } from '@curvenote/schema';
const schema = new Schema(schemas.presets.full);
export const tnodes = builders(schema, {
p: { nodeType: 'paragraph' },
h1: { nodeType: 'heading', level: 1 },
h2: { nodeType: 'heading', level: 2 },
hr: { nodeType: 'horizontal_rule' },
li: { nodeType: 'list_item' },
ol: { nodeType: 'ordered_list' },
ol3: { nodeType: 'ordered_list', order: 3 },
ul: { nodeType: 'bullet_list' },
pre: { nodeType: 'code_block' },
br: { nodeType: 'hard_break' },
img: { nodeType: 'image', src: 'img.png', alt: 'x' },
a: { markType: 'link', href: 'https://example.com' },
math: { nodeType: 'math' },
equation: { nodeType: 'equation', numbered: true, id: 'eq1' },
equationUnnumbered: { nodeType: 'equation', numbered: false, id: 'eq2' },
abbr: { nodeType: 'abbr', title: 'Cascading Style Sheets' },
aside: { nodeType: 'aside' },
figure: { nodeType: 'figure' },
}) as any;
export const tdoc = (...args: Parameters<typeof tnodes.doc>) => tnodes.doc('', ...args);
@@ -1,109 +0,0 @@
import * as fs from 'fs';
import { describe, it, expect } from 'vitest';
import {
DocxSerializerAsync,
defaultAsyncNodes,
defaultMarks,
defaultDocxSerializer,
writeDocx,
} from '../src';
import { tnodes, tdoc } from './build';
import { writeFileSync } from 'fs';
const {
blockquote,
h1,
h2,
p,
hr,
li,
ol,
ol3,
ul,
pre,
em,
strong,
code,
a,
br,
img,
math,
equation,
equationUnnumbered,
figure,
} = tnodes;
const imageBase64Data = `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`;
/**
* Adds image type to base64 encoded images
*/
export const docxSerializer = new DocxSerializerAsync(
{
...defaultAsyncNodes,
async image(state, node) {
const { src } = node.attrs;
await state.image(src, 70, 'center', undefined, 'png');
state.closeBlock(node);
},
},
defaultMarks,
);
describe('DOCX Serialization', () => {
it('serializes document structure with async image handling', async () => {
const w = await docxSerializer.serializeAsync(
tdoc(
h1('Welcome to ', code('prosemirror-docx'), strong('!!')),
p('This is ', code('code'), br(), 'hello!'),
ul(li(p('bullet 1')), li(p('bullet 2')), ul(li(p('bullet 3.1')), li(p('bullet 3.2')))),
ul(li(p('bullet 1')), li(p('bullet 2')), ul(li(p('bullet 3.1')), li(p('bullet 3.2')))),
ol(li(p('bullet 1')), li(p('bullet 2')), ul(li(p('bullet 3.1')), li(p('bullet 3.2')))),
p(a('This is '), a(em('emphasized'))),
hr(),
p('Some math in a paragraph: ', math('Ax=b'), ' and then a standalone numbered equation:'),
equation('Ax=b'),
p('And an unnumbered equation:'),
equationUnnumbered('\\sum^{9}_{i=0}i+2 = ??'),
img({ src: 'https://avatars.githubusercontent.com/u/78044536' }),
img({ src: `data:text/plain;base64,${imageBase64Data}` }),
),
{
async getImageBuffer(src: string) {
const arrayBuffer = await fetch(src).then((res) => res.arrayBuffer());
return new Uint8Array(arrayBuffer);
},
},
);
const buffer = await writeDocx(w);
fs.writeFileSync(`hello-async.docx`, buffer);
expect(1).toBe(1);
});
it('serializes document structure with sync image handling', async () => {
const w = defaultDocxSerializer.serialize(
tdoc(
h1('Welcome to ', code('prosemirror-docx'), strong('!!')),
p('This is ', code('code'), br(), 'hello!'),
ul(li(p('bullet 1')), li(p('bullet 2')), ul(li(p('bullet 3.1')), li(p('bullet 3.2')))),
ul(li(p('bullet 1')), li(p('bullet 2')), ul(li(p('bullet 3.1')), li(p('bullet 3.2')))),
ol(li(p('bullet 1')), li(p('bullet 2')), ul(li(p('bullet 3.1')), li(p('bullet 3.2')))),
p(a('This is '), a(em('emphasized'))),
hr(),
p('Some math in a paragraph: ', math('Ax=b'), ' and then a standalone numbered equation:'),
equation('Ax=b'),
p('And an unnumbered equation:'),
equationUnnumbered('\\sum^{9}_{i=0}i+2 = ??'),
img(),
),
{
getImageBuffer(src: string) {
return Buffer.from(imageBase64Data, 'base64');
},
},
);
await writeDocx(w).then((buffer) => {
fs.writeFileSync('hello.docx', buffer);
});
expect(2).toBe(2);
});
});
@@ -14,14 +14,16 @@ export function createShortId() {
} }
export function buildDoc(state: SerializationState, opts?: IPropertiesOptions): Document { export function buildDoc(state: SerializationState, opts?: IPropertiesOptions): Document {
let sections = state?.sections?.map((section) => ({ let sections = state?.sections?.length
properties: section.config.properties || { ? state.sections.map((section) => ({
type: SectionType.CONTINUOUS, properties: section.config.properties || {
}, type: SectionType.CONTINUOUS,
headers: section.config.headers, },
footers: section.config.footers, headers: section.config.headers,
children: section.children, footers: section.config.footers,
})); children: section.children,
}))
: undefined;
if (!sections) { if (!sections) {
sections = [ sections = [
{ {
+82 -41
View File
@@ -53,7 +53,7 @@ importers:
.: .:
dependencies: dependencies:
'@braintree/sanitize-url': '@braintree/sanitize-url':
specifier: ^7.1.2 specifier: 7.1.2
version: 7.1.2 version: 7.1.2
'@casl/ability': '@casl/ability':
specifier: 6.8.0 specifier: 6.8.0
@@ -62,7 +62,7 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:packages/editor-ext version: link:packages/editor-ext
'@floating-ui/dom': '@floating-ui/dom':
specifier: ^1.7.3 specifier: 1.7.3
version: 1.7.3 version: 1.7.3
'@hocuspocus/provider': '@hocuspocus/provider':
specifier: 3.4.4 specifier: 3.4.4
@@ -74,10 +74,10 @@ importers:
specifier: 3.4.4 specifier: 3.4.4
version: 3.4.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30) version: 3.4.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)
'@joplin/turndown': '@joplin/turndown':
specifier: ^4.0.82 specifier: 4.0.82
version: 4.0.82 version: 4.0.82
'@joplin/turndown-plugin-gfm': '@joplin/turndown-plugin-gfm':
specifier: ^1.0.64 specifier: 1.0.64
version: 1.0.64 version: 1.0.64
'@sindresorhus/slugify': '@sindresorhus/slugify':
specifier: 3.0.0 specifier: 3.0.0
@@ -170,34 +170,37 @@ importers:
specifier: 3.0.2 specifier: 3.0.2
version: 3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) version: 3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
bytes: bytes:
specifier: ^3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
cross-env: cross-env:
specifier: ^10.1.0 specifier: 10.1.0
version: 10.1.0 version: 10.1.0
date-fns: date-fns:
specifier: ^4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
diff: diff:
specifier: 8.0.3 specifier: 8.0.3
version: 8.0.3 version: 8.0.3
docx:
specifier: 9.7.1
version: 9.7.1
dompurify: dompurify:
specifier: 3.4.1 specifier: 3.4.1
version: 3.4.1 version: 3.4.1
fractional-indexing-jittered: fractional-indexing-jittered:
specifier: ^1.0.0 specifier: 1.0.0
version: 1.0.0 version: 1.0.0
highlight.js: highlight.js:
specifier: ^11.11.1 specifier: 11.11.1
version: 11.11.1 version: 11.11.1
image-dimensions: image-dimensions:
specifier: ^2.5.0 specifier: 2.5.0
version: 2.5.0 version: 2.5.0
jszip: jszip:
specifier: ^3.10.1 specifier: 3.10.1
version: 3.10.1 version: 3.10.1
linkifyjs: linkifyjs:
specifier: ^4.3.2 specifier: 4.3.2
version: 4.3.2 version: 4.3.2
marked: marked:
specifier: 17.0.5 specifier: 17.0.5
@@ -206,16 +209,16 @@ importers:
specifier: 3.0.0-canary.1 specifier: 3.0.0-canary.1
version: 3.0.0-canary.1 version: 3.0.0-canary.1
qrcode: qrcode:
specifier: ^1.5.4 specifier: 1.5.4
version: 1.5.4 version: 1.5.4
rfc6902: rfc6902:
specifier: 5.2.0 specifier: 5.2.0
version: 5.2.0 version: 5.2.0
uuid: uuid:
specifier: ^14.0.0 specifier: 14.0.0
version: 14.0.0 version: 14.0.0
y-indexeddb: y-indexeddb:
specifier: ^9.0.12 specifier: 9.0.12
version: 9.0.12(yjs@13.6.30) version: 9.0.12(yjs@13.6.30)
y-prosemirror: y-prosemirror:
specifier: 1.3.7 specifier: 1.3.7
@@ -228,16 +231,16 @@ importers:
specifier: 22.6.1 specifier: 22.6.1
version: 22.6.1(@babel/traverse@7.28.5)(nx@22.6.1) version: 22.6.1(@babel/traverse@7.28.5)(nx@22.6.1)
'@types/bytes': '@types/bytes':
specifier: ^3.1.5 specifier: 3.1.5
version: 3.1.5 version: 3.1.5
'@types/qrcode': '@types/qrcode':
specifier: ^1.5.6 specifier: 1.5.6
version: 1.5.6 version: 1.5.6
'@types/turndown': '@types/turndown':
specifier: ^5.0.6 specifier: 5.0.6
version: 5.0.6 version: 5.0.6
concurrently: concurrently:
specifier: ^9.2.1 specifier: 9.2.1
version: 9.2.1 version: 9.2.1
nx: nx:
specifier: 22.6.1 specifier: 22.6.1
@@ -484,13 +487,13 @@ importers:
apps/server: apps/server:
dependencies: dependencies:
'@ai-sdk/google': '@ai-sdk/google':
specifier: ^3.0.52 specifier: 3.0.52
version: 3.0.52(zod@4.3.6) version: 3.0.52(zod@4.3.6)
'@ai-sdk/openai': '@ai-sdk/openai':
specifier: ^3.0.47 specifier: 3.0.47
version: 3.0.47(zod@4.3.6) version: 3.0.47(zod@4.3.6)
'@ai-sdk/openai-compatible': '@ai-sdk/openai-compatible':
specifier: ^2.0.37 specifier: 2.0.37
version: 2.0.37(zod@4.3.6) version: 2.0.37(zod@4.3.6)
'@aws-sdk/client-s3': '@aws-sdk/client-s3':
specifier: 3.1050.0 specifier: 3.1050.0
@@ -505,7 +508,7 @@ importers:
specifier: 12.31.0 specifier: 12.31.0
version: 12.31.0 version: 12.31.0
'@clickhouse/client': '@clickhouse/client':
specifier: ^1.18.2 specifier: 1.18.2
version: 1.18.2 version: 1.18.2
'@docmost/pdf-inspector': '@docmost/pdf-inspector':
specifier: 1.9.6 specifier: 1.9.6
@@ -589,43 +592,43 @@ importers:
specifier: ^8.3.0 specifier: ^8.3.0
version: 8.3.0(socket.io-adapter@2.5.4) version: 8.3.0(socket.io-adapter@2.5.4)
ai: ai:
specifier: ^6.0.134 specifier: 6.0.134
version: 6.0.134(zod@4.3.6) version: 6.0.134(zod@4.3.6)
ai-sdk-ollama: ai-sdk-ollama:
specifier: ^3.8.1 specifier: 3.8.1
version: 3.8.1(ai@6.0.134(zod@4.3.6))(zod@4.3.6) version: 3.8.1(ai@6.0.134(zod@4.3.6))(zod@4.3.6)
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: 6.0.0
version: 6.0.0 version: 6.0.0
bowser: bowser:
specifier: ^2.14.1 specifier: 2.14.1
version: 2.14.1 version: 2.14.1
bullmq: bullmq:
specifier: ^5.76.10 specifier: 5.76.10
version: 5.76.10 version: 5.76.10
cache-manager: cache-manager:
specifier: ^7.2.8 specifier: 7.2.8
version: 7.2.8 version: 7.2.8
cheerio: cheerio:
specifier: ^1.2.0 specifier: 1.2.0
version: 1.2.0 version: 1.2.0
class-transformer: class-transformer:
specifier: ^0.5.1 specifier: 0.5.1
version: 0.5.1 version: 0.5.1
class-validator: class-validator:
specifier: ^0.15.1 specifier: 0.15.1
version: 0.15.1 version: 0.15.1
cookie: cookie:
specifier: ^1.1.1 specifier: 1.1.1
version: 1.1.1 version: 1.1.1
fast-bm25: fast-bm25:
specifier: 0.0.5 specifier: 0.0.5
version: 0.0.5(typescript@5.9.3) version: 0.0.5(typescript@5.9.3)
fastify-ip: fastify-ip:
specifier: ^2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
fs-extra: fs-extra:
specifier: ^11.3.4 specifier: 11.3.4
version: 11.3.4 version: 11.3.4
happy-dom: happy-dom:
specifier: 20.8.9 specifier: 20.8.9
@@ -736,13 +739,13 @@ importers:
specifier: ^17.7.0 specifier: ^17.7.0
version: 17.7.0 version: 17.7.0
tlds: tlds:
specifier: ^1.261.0 specifier: 1.261.0
version: 1.261.0 version: 1.261.0
tmp-promise: tmp-promise:
specifier: ^3.0.3 specifier: 3.0.3
version: 3.0.3 version: 3.0.3
tseep: tseep:
specifier: ^1.3.1 specifier: 1.3.1
version: 1.3.1 version: 1.3.1
typesense: typesense:
specifier: ^3.0.5 specifier: ^3.0.5
@@ -6306,6 +6309,10 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
docx@9.7.1:
resolution: {integrity: sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==}
engines: {node: '>=10'}
dom-accessibility-api@0.5.16: dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -6977,6 +6984,9 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hash.js@1.1.7:
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
hashery@1.4.0: hashery@1.4.0:
resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==} resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -8111,6 +8121,9 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'} engines: {node: '>=4'}
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
minimatch@10.2.4: minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -10264,6 +10277,10 @@ packages:
xml-encryption@3.1.0: xml-encryption@3.1.0:
resolution: {integrity: sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==} resolution: {integrity: sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==}
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
xml-name-validator@5.0.0: xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -10276,6 +10293,9 @@ packages:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
xml@1.0.1:
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
xmlbuilder@10.1.1: xmlbuilder@10.1.1:
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -14449,7 +14469,7 @@ snapshots:
'@tiptap/extension-bubble-menu@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': '@tiptap/extension-bubble-menu@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies: dependencies:
'@floating-ui/dom': 1.7.4 '@floating-ui/dom': 1.7.3
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4 '@tiptap/pm': 3.20.4
optional: true optional: true
@@ -16620,6 +16640,15 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
docx@9.7.1:
dependencies:
'@types/node': 25.5.0
hash.js: 1.1.7
jszip: 3.10.1
nanoid: 5.1.7
xml: 1.0.1
xml-js: 1.6.11
dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {} dom-accessibility-api@0.6.3: {}
@@ -17541,6 +17570,11 @@ snapshots:
dependencies: dependencies:
has-symbols: 1.1.0 has-symbols: 1.1.0
hash.js@1.1.7:
dependencies:
inherits: 2.0.4
minimalistic-assert: 1.0.1
hashery@1.4.0: hashery@1.4.0:
dependencies: dependencies:
hookified: 1.15.1 hookified: 1.15.1
@@ -18821,6 +18855,8 @@ snapshots:
min-indent@1.0.1: {} min-indent@1.0.1: {}
minimalistic-assert@1.0.1: {}
minimatch@10.2.4: minimatch@10.2.4:
dependencies: dependencies:
brace-expansion: 5.0.6 brace-expansion: 5.0.6
@@ -20163,8 +20199,7 @@ snapshots:
sax@1.4.1: {} sax@1.4.1: {}
sax@1.6.0: sax@1.6.0: {}
optional: true
saxes@6.0.0: saxes@6.0.0:
dependencies: dependencies:
@@ -21223,6 +21258,10 @@ snapshots:
escape-html: 1.0.3 escape-html: 1.0.3
xpath: 0.0.32 xpath: 0.0.32
xml-js@1.6.11:
dependencies:
sax: 1.6.0
xml-name-validator@5.0.0: {} xml-name-validator@5.0.0: {}
xml-naming@0.1.0: {} xml-naming@0.1.0: {}
@@ -21232,6 +21271,8 @@ snapshots:
sax: 1.4.1 sax: 1.4.1
xmlbuilder: 11.0.1 xmlbuilder: 11.0.1
xml@1.0.1: {}
xmlbuilder@10.1.1: {} xmlbuilder@10.1.1: {}
xmlbuilder@11.0.1: {} xmlbuilder@11.0.1: {}