mirror of
https://github.com/docmost/docmost.git
synced 2026-06-22 12:01:39 +10:00
feat(ee): docx word export
This commit is contained in:
@@ -6,13 +6,21 @@ import {
|
||||
Select,
|
||||
Switch,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} 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 { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { exportSpace } from "@/features/space/services/space-service";
|
||||
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 {
|
||||
id: string;
|
||||
@@ -32,17 +40,25 @@ export default function ExportModal({
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
|
||||
const [isExporting, setIsExporting] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const isDocx = format === ExportFormat.Docx;
|
||||
const docxEntitled = useHasFeature(Feature.DOCX_EXPORT);
|
||||
const blockedByLicense = isDocx && !docxEntitled;
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
if (type === "page") {
|
||||
await exportPage({
|
||||
pageId: id,
|
||||
format,
|
||||
includeChildren,
|
||||
includeAttachments,
|
||||
});
|
||||
if (format === ExportFormat.Docx) {
|
||||
await exportPageToDocx({ pageId: id });
|
||||
} else {
|
||||
await exportPage({
|
||||
pageId: id,
|
||||
format,
|
||||
includeChildren,
|
||||
includeAttachments,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (type === "space") {
|
||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||
@@ -88,10 +104,15 @@ export default function ExportModal({
|
||||
<div>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
<ExportFormatSelection
|
||||
format={format}
|
||||
onChange={handleChange}
|
||||
includeDocx={type === "page"}
|
||||
docxEntitled={docxEntitled}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{type === "page" && (
|
||||
{type === "page" && !isDocx && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
@@ -143,7 +164,16 @@ export default function ExportModal({
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</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>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
@@ -154,23 +184,49 @@ export default function ExportModal({
|
||||
interface ExportFormatSelection {
|
||||
format: ExportFormat;
|
||||
onChange: (value: string) => void;
|
||||
includeDocx?: boolean;
|
||||
docxEntitled?: boolean;
|
||||
}
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
function ExportFormatSelection({
|
||||
format,
|
||||
onChange,
|
||||
includeDocx,
|
||||
docxEntitled,
|
||||
}: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = [
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
...(includeDocx
|
||||
? [{ value: "docx", label: "Word (.docx)", disabled: !docxEntitled }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
]}
|
||||
data={data}
|
||||
defaultValue={format}
|
||||
onChange={onChange}
|
||||
styles={{ wrapper: { maxWidth: 120 } }}
|
||||
comboboxProps={{ width: "120" }}
|
||||
styles={{ wrapper: { maxWidth: 140 }, option: { opacity: 1 } }}
|
||||
comboboxProps={{ width: 200 }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ export const Feature = {
|
||||
SHARING_CONTROLS: 'sharing:controls',
|
||||
TEMPLATES: 'templates',
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
} as const;
|
||||
|
||||
@@ -132,6 +132,25 @@ export async function exportPage(data: IExportPageParams): Promise<void> {
|
||||
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) {
|
||||
const formData = new FormData();
|
||||
formData.append("spaceId", spaceId);
|
||||
|
||||
@@ -98,4 +98,5 @@ export interface IExportPageParams {
|
||||
export enum ExportFormat {
|
||||
HTML = "html",
|
||||
Markdown = "markdown",
|
||||
Docx = "docx",
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ const api: AxiosInstance = axios.create({
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
// 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) {
|
||||
const path = new URL(response.request.responseURL)?.pathname;
|
||||
if (path && exemptEndpoints.includes(path)) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
||||
+21
-20
@@ -30,14 +30,14 @@
|
||||
"test:e2e": "jest --config test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^3.0.52",
|
||||
"@ai-sdk/openai": "^3.0.47",
|
||||
"@ai-sdk/openai-compatible": "^2.0.37",
|
||||
"@ai-sdk/google": "3.0.52",
|
||||
"@ai-sdk/openai": "3.0.47",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@aws-sdk/client-s3": "3.1050.0",
|
||||
"@aws-sdk/lib-storage": "3.1050.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.1050.0",
|
||||
"@azure/storage-blob": "12.31.0",
|
||||
"@clickhouse/client": "^1.18.2",
|
||||
"@clickhouse/client": "1.18.2",
|
||||
"@docmost/pdf-inspector": "1.9.6",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/multipart": "^10.0.0",
|
||||
@@ -65,19 +65,19 @@
|
||||
"@nestjs/websockets": "^11.1.19",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"ai": "^6.0.134",
|
||||
"ai-sdk-ollama": "^3.8.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bowser": "^2.14.1",
|
||||
"bullmq": "^5.76.10",
|
||||
"cache-manager": "^7.2.8",
|
||||
"cheerio": "^1.2.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"cookie": "^1.1.1",
|
||||
"ai": "6.0.134",
|
||||
"ai-sdk-ollama": "3.8.1",
|
||||
"bcrypt": "6.0.0",
|
||||
"bowser": "2.14.1",
|
||||
"bullmq": "5.76.10",
|
||||
"cache-manager": "7.2.8",
|
||||
"cheerio": "1.2.0",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.15.1",
|
||||
"cookie": "1.1.1",
|
||||
"fast-bm25": "0.0.5",
|
||||
"fastify-ip": "^2.0.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"fastify-ip": "2.0.0",
|
||||
"fs-extra": "11.3.4",
|
||||
"happy-dom": "20.8.9",
|
||||
"ioredis": "^5.10.1",
|
||||
"js-tiktoken": "^1.0.21",
|
||||
@@ -114,9 +114,9 @@
|
||||
"scimmy": "1.3.5",
|
||||
"socket.io": "^4.8.3",
|
||||
"stripe": "^17.7.0",
|
||||
"tlds": "^1.261.0",
|
||||
"tmp-promise": "^3.0.3",
|
||||
"tseep": "^1.3.1",
|
||||
"tlds": "1.261.0",
|
||||
"tmp-promise": "3.0.3",
|
||||
"tseep": "1.3.1",
|
||||
"typesense": "^3.0.5",
|
||||
"undici": "7.24.0",
|
||||
"ws": "^8.20.1",
|
||||
@@ -192,7 +192,8 @@
|
||||
"moduleNameMapper": {
|
||||
"^@docmost/db/(.*)$": "<rootDir>/database/$1",
|
||||
"^@docmost/transactional/(.*)$": "<rootDir>/integrations/transactional/$1",
|
||||
"^@docmost/ee/(.*)$": "<rootDir>/ee/$1"
|
||||
"^@docmost/ee/(.*)$": "<rootDir>/ee/$1",
|
||||
"^src/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const Feature = {
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
TEMPLATES: 'templates',
|
||||
PDF_EXPORT: 'export:pdf',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
} as const;
|
||||
|
||||
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: e7320a5a0f...090595fb28
+22
-21
@@ -19,15 +19,15 @@
|
||||
"clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.2",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@casl/ability": "6.8.0",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@floating-ui/dom": "1.7.3",
|
||||
"@hocuspocus/provider": "3.4.4",
|
||||
"@hocuspocus/server": "3.4.4",
|
||||
"@hocuspocus/transformer": "3.4.4",
|
||||
"@joplin/turndown": "^4.0.82",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.64",
|
||||
"@joplin/turndown": "4.0.82",
|
||||
"@joplin/turndown-plugin-gfm": "1.0.64",
|
||||
"@sindresorhus/slugify": "3.0.0",
|
||||
"@tiptap/core": "3.20.4",
|
||||
"@tiptap/extension-audio": "3.20.4",
|
||||
@@ -58,31 +58,32 @@
|
||||
"@tiptap/starter-kit": "3.20.4",
|
||||
"@tiptap/suggestion": "3.20.4",
|
||||
"@tiptap/y-tiptap": "3.0.2",
|
||||
"bytes": "^3.1.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"bytes": "3.1.2",
|
||||
"cross-env": "10.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"diff": "8.0.3",
|
||||
"docx": "9.7.1",
|
||||
"dompurify": "3.4.1",
|
||||
"fractional-indexing-jittered": "^1.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"image-dimensions": "^2.5.0",
|
||||
"jszip": "^3.10.1",
|
||||
"linkifyjs": "^4.3.2",
|
||||
"fractional-indexing-jittered": "1.0.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"image-dimensions": "2.5.0",
|
||||
"jszip": "3.10.1",
|
||||
"linkifyjs": "4.3.2",
|
||||
"marked": "17.0.5",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode": "1.5.4",
|
||||
"rfc6902": "5.2.0",
|
||||
"uuid": "^14.0.0",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"uuid": "14.0.0",
|
||||
"y-indexeddb": "9.0.12",
|
||||
"y-prosemirror": "1.3.7",
|
||||
"yjs": "^13.6.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nx/js": "22.6.1",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"concurrently": "^9.2.1",
|
||||
"@types/bytes": "3.1.5",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/turndown": "5.0.6",
|
||||
"concurrently": "9.2.1",
|
||||
"nx": "22.6.1",
|
||||
"tsx": "^4.21.0"
|
||||
},
|
||||
@@ -133,8 +134,8 @@
|
||||
"axios": "1.16.0",
|
||||
"langsmith": "0.7.0",
|
||||
"follow-redirects": "1.16.0",
|
||||
"protobufjs": "7.5.8",
|
||||
"ip-address": "10.1.1"
|
||||
"protobufjs": "7.5.8",
|
||||
"ip-address": "10.1.1"
|
||||
},
|
||||
"neverBuiltDependencies": []
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@docmost/editor-ext",
|
||||
"homepage": "https://docmost.com",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"dev": "tsc --watch"
|
||||
|
||||
@@ -34,3 +34,7 @@ export * from "./lib/pdf";
|
||||
export * from "./lib/page-break";
|
||||
export * from "./lib/resizable-nodeview";
|
||||
|
||||
export {
|
||||
pageNodeToDocxBuffer,
|
||||
type DocxImageResolver,
|
||||
} from "./lib/prosemirror-docx";
|
||||
|
||||
@@ -16,10 +16,9 @@ export {
|
||||
MAX_IMAGE_WIDTH,
|
||||
} from './serializer';
|
||||
export {
|
||||
defaultDocxSerializer,
|
||||
defaultDocxSerializerAsync,
|
||||
defaultAsyncNodes,
|
||||
defaultNodes,
|
||||
defaultMarks,
|
||||
pageNodeToDocxBuffer,
|
||||
type DocxImageResolver,
|
||||
} from './schema';
|
||||
export { writeDocx, createDocFromState, buildDoc } from './utils';
|
||||
|
||||
@@ -1,76 +1,67 @@
|
||||
import { HeadingLevel, ShadingType } from 'docx';
|
||||
import { Node } from 'prosemirror-model';
|
||||
import {
|
||||
DocxSerializer,
|
||||
MarkSerializer,
|
||||
NodeSerializer,
|
||||
DocxSerializerAsync,
|
||||
MarkSerializer,
|
||||
NodeSerializerAsync,
|
||||
OptionsAsync,
|
||||
} from './serializer';
|
||||
import { getLatexFromNode } from './utils';
|
||||
import { writeDocx } from './utils';
|
||||
|
||||
export const defaultNodes: NodeSerializer = {
|
||||
text(state, node) {
|
||||
state.text(node.text ?? '');
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
heading(state, node) {
|
||||
state.renderInline(node);
|
||||
const heading = [
|
||||
HeadingLevel.HEADING_1,
|
||||
HeadingLevel.HEADING_2,
|
||||
HeadingLevel.HEADING_3,
|
||||
HeadingLevel.HEADING_4,
|
||||
HeadingLevel.HEADING_5,
|
||||
HeadingLevel.HEADING_6,
|
||||
][node.attrs.level - 1];
|
||||
state.closeBlock(node, { heading });
|
||||
},
|
||||
blockquote(state, node) {
|
||||
state.renderContent(node, { style: 'IntenseQuote' });
|
||||
},
|
||||
code_block(state, node) {
|
||||
// TODO: something for code
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
horizontal_rule(state, node) {
|
||||
// Kinda hacky, but this works to insert two paragraphs, the first with a break
|
||||
state.closeBlock(node, { thematicBreak: true });
|
||||
state.closeBlock(node);
|
||||
},
|
||||
hard_break(state) {
|
||||
state.addRunOptions({ break: 1 });
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
state.renderList(node, 'numbered');
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, 'bullets');
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderListItem(node);
|
||||
},
|
||||
// Presentational
|
||||
image(state, node) {
|
||||
const { src } = node.attrs;
|
||||
state.image(src);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
// Technical
|
||||
math(state, node) {
|
||||
state.math(getLatexFromNode(node), { inline: true });
|
||||
},
|
||||
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 type DocxImageResolver = OptionsAsync['getImageBuffer'];
|
||||
|
||||
// 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
|
||||
// (named colors, hsl, etc.) so the caller omits the color rather than letting
|
||||
// docx throw "Invalid hex value".
|
||||
function toDocxColor(input?: string): string | undefined {
|
||||
if (!input) return undefined;
|
||||
const value = input.trim().toLowerCase();
|
||||
const hex = value.startsWith('#') ? value.slice(1) : value;
|
||||
if (/^[0-9a-f]{6}$/.test(hex)) return hex;
|
||||
if (/^[0-9a-f]{3}$/.test(hex)) {
|
||||
return hex
|
||||
.split('')
|
||||
.map((ch) => ch + ch)
|
||||
.join('');
|
||||
}
|
||||
const rgb = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgb) {
|
||||
const channel = (n: string) =>
|
||||
Math.max(0, Math.min(255, parseInt(n, 10)))
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
return channel(rgb[1]) + channel(rgb[2]) + channel(rgb[3]);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Images and diagrams embed via the image resolver; the URL (with its file
|
||||
// extension) is passed through so docx can infer the image type.
|
||||
const renderImage: NodeSerializerAsync[string] = async (state, node) => {
|
||||
const src = node.attrs?.src || node.attrs?.attachmentId;
|
||||
if (src) {
|
||||
try {
|
||||
await state.image(src, 100);
|
||||
} catch {
|
||||
// Unrenderable/missing image: skip rather than fail the whole export.
|
||||
}
|
||||
}
|
||||
state.closeBlock(node);
|
||||
};
|
||||
|
||||
// Non-embeddable media render as a labelled line.
|
||||
const renderFileLine: NodeSerializerAsync[string] = (state, node) => {
|
||||
const label =
|
||||
node.attrs?.name || node.attrs?.src || node.attrs?.url || 'attachment';
|
||||
state.text(label);
|
||||
state.closeBlock(node);
|
||||
};
|
||||
|
||||
const renderEmbedLine: NodeSerializerAsync[string] = (state, node) => {
|
||||
const label = node.attrs?.src || node.attrs?.url || 'embed';
|
||||
state.text(label);
|
||||
state.closeBlock(node);
|
||||
};
|
||||
|
||||
export const defaultAsyncNodes: NodeSerializerAsync = {
|
||||
@@ -90,111 +81,170 @@ export const defaultAsyncNodes: NodeSerializerAsync = {
|
||||
HeadingLevel.HEADING_4,
|
||||
HeadingLevel.HEADING_5,
|
||||
HeadingLevel.HEADING_6,
|
||||
][node.attrs.level - 1];
|
||||
][(node.attrs.level ?? 1) - 1];
|
||||
state.closeBlock(node, { heading });
|
||||
},
|
||||
blockquote(state, node) {
|
||||
state.renderContent(node, { style: 'IntenseQuote' });
|
||||
async blockquote(state, node) {
|
||||
await state.renderContent(node, { style: 'IntenseQuote' });
|
||||
},
|
||||
code_block(state, node) {
|
||||
// TODO: something for code
|
||||
state.renderContent(node);
|
||||
async codeBlock(state, node) {
|
||||
await state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
horizontal_rule(state, node) {
|
||||
// Kinda hacky, but this works to insert two paragraphs, the first with a break
|
||||
horizontalRule(state, node) {
|
||||
state.closeBlock(node, { thematicBreak: true });
|
||||
state.closeBlock(node);
|
||||
},
|
||||
hard_break(state) {
|
||||
hardBreak(state) {
|
||||
state.addRunOptions({ break: 1 });
|
||||
},
|
||||
async ordered_list(state, node) {
|
||||
await state.renderList(node, 'numbered');
|
||||
},
|
||||
async bullet_list(state, node) {
|
||||
async bulletList(state, node) {
|
||||
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);
|
||||
},
|
||||
// Presentational
|
||||
async image(state, node) {
|
||||
const { src } = node.attrs;
|
||||
await state.image(src);
|
||||
state.closeBlock(node);
|
||||
async taskList(state, node) {
|
||||
await state.renderList(node, 'bullets');
|
||||
},
|
||||
// Technical
|
||||
math(state, node) {
|
||||
state.math(getLatexFromNode(node), { inline: true });
|
||||
},
|
||||
equation(state, node) {
|
||||
const { id, numbered } = node.attrs;
|
||||
state.math(getLatexFromNode(node), { inline: false, numbered, id });
|
||||
state.closeBlock(node);
|
||||
async taskItem(state, node) {
|
||||
if (state.currentNumbering) {
|
||||
state.addParagraphOptions({ numbering: state.currentNumbering });
|
||||
}
|
||||
state.text(node.attrs?.checked ? '☑ ' : '☐ ');
|
||||
await state.renderContent(node);
|
||||
},
|
||||
async table(state, 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 = {
|
||||
em() {
|
||||
return { italics: true };
|
||||
},
|
||||
strong() {
|
||||
bold() {
|
||||
return { bold: true };
|
||||
},
|
||||
italic() {
|
||||
return { italics: true };
|
||||
},
|
||||
bold() {
|
||||
return { bold: true };
|
||||
strike() {
|
||||
return { strike: true };
|
||||
},
|
||||
link() {
|
||||
// Note, this is handled specifically in the serializer
|
||||
// Word treats links more like a Node rather than a mark
|
||||
return {};
|
||||
underline() {
|
||||
return { underline: {} };
|
||||
},
|
||||
code() {
|
||||
return {
|
||||
font: {
|
||||
name: 'Monospace',
|
||||
},
|
||||
font: { name: 'Monospace' },
|
||||
color: '000000',
|
||||
shading: {
|
||||
type: ShadingType.SOLID,
|
||||
color: 'D2D3D2',
|
||||
fill: 'D2D3D2',
|
||||
},
|
||||
shading: { type: ShadingType.SOLID, color: 'D2D3D2', fill: 'D2D3D2' },
|
||||
};
|
||||
},
|
||||
abbr() {
|
||||
// TODO: abbreviation
|
||||
return {};
|
||||
},
|
||||
subscript() {
|
||||
return { subScript: true };
|
||||
},
|
||||
superscript() {
|
||||
return { superScript: true };
|
||||
},
|
||||
strikethrough() {
|
||||
// doubleStrike!
|
||||
return { strike: true };
|
||||
subscript() {
|
||||
return { subScript: true };
|
||||
},
|
||||
underline() {
|
||||
return {
|
||||
underline: {},
|
||||
};
|
||||
link() {
|
||||
// Handled specifically in the serializer; Word treats links as nodes.
|
||||
return {};
|
||||
},
|
||||
smallcaps() {
|
||||
return { smallCaps: true };
|
||||
highlight(_state, _node, mark) {
|
||||
const fill = toDocxColor(mark.attrs?.color);
|
||||
return fill
|
||||
? { shading: { type: ShadingType.CLEAR, fill } }
|
||||
: { highlight: 'yellow' };
|
||||
},
|
||||
allcaps() {
|
||||
return { allCaps: true };
|
||||
// @tiptap/extension-color stores the color on the textStyle mark.
|
||||
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 const defaultDocxSerializerAsync = new DocxSerializerAsync(defaultAsyncNodes, defaultMarks);
|
||||
export async function pageNodeToDocxBuffer(
|
||||
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) {
|
||||
this.nodes = nodes;
|
||||
this.marks = marks;
|
||||
this.options = options ?? {};
|
||||
this.options = options ?? ({} as Options);
|
||||
this.children = [];
|
||||
this.numbering = [];
|
||||
|
||||
@@ -342,7 +342,7 @@ export class DocxSerializerState {
|
||||
// Check if all cells are headers in this row
|
||||
let tableHeader = true;
|
||||
row.content.forEach((cell) => {
|
||||
if (cell.type.name !== 'table_header') {
|
||||
if (cell.type.name !== 'tableHeader') {
|
||||
tableHeader = false;
|
||||
}
|
||||
});
|
||||
@@ -529,7 +529,7 @@ export class DocxSerializerStateAsync {
|
||||
constructor(nodes: NodeSerializerAsync, marks: MarkSerializer, options: OptionsAsync) {
|
||||
this.nodes = nodes;
|
||||
this.marks = marks;
|
||||
this.options = options ?? {};
|
||||
this.options = options ?? ({} as OptionsAsync);
|
||||
this.children = [];
|
||||
this.numbering = [];
|
||||
|
||||
@@ -765,7 +765,7 @@ export class DocxSerializerStateAsync {
|
||||
// Check if all cells in the row are headers
|
||||
for (let cellIndex = 0; cellIndex < row.content.childCount; cellIndex += 1) {
|
||||
const cell = row.content.child(cellIndex);
|
||||
if (cell.type.name !== 'table_header') {
|
||||
if (cell.type.name !== 'tableHeader') {
|
||||
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 = `iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAACzVBMVEUAAAAAAAAAAAAAAAA/AD8zMzMqKiokJCQfHx8cHBwZGRkuFxcqFSonJyckJCQiIiIfHx8eHh4cHBwoGhomGSYkJCQhISEfHx8eHh4nHR0lHBwkGyQjIyMiIiIgICAfHx8mHh4lHh4kHR0jHCMiGyIhISEgICAfHx8lHx8kHh4jHR0hHCEhISEgICAlHx8kHx8jHh4jHh4iHSIhHCEhISElICAkHx8jHx8jHh4iHh4iHSIhHSElICAkICAjHx8jHx8iHh4iHh4hHiEhHSEkICAjHx8iHx8iHx8hHh4hHiEkHSEjHSAjHx8iHx8iHx8hHh4kHiEkHiEjHSAiHx8hHx8hHh4kHiEjHiAjHSAiHx8iHx8hHx8kHh4jHiEjHiAjHiAiICAiHx8kHx8jHh4jHiEjHiAiHiAiHSAiHx8jHx8jHx8jHiAiHiAiHiAiHSAiHx8jHx8jHx8iHiAiHiAiHiAjHx8jHx8jHx8jHx8iHiAiHiAiHiAjHx8jHx8jHx8iHx8iHSAiHiAjHiAjHx8jHx8hHx8iHx8iHyAiHiAjHiAjHiAjHh4hHx8iHx8iHx8iHyAjHSAjHiAjHiAjHh4hHx8iHx8iHx8jHyAjHiAhHh4iHx8iHx8jHyAjHSAjHSAhHiAhHh4iHx8iHx8jHx8jHyAjHSAjHSAiHh4iHh4jHx8jHx8jHyAjHyAhHSAhHSAiHh4iHh4jHx8jHx8jHyAhHyAhHSAiHSAiHh4jHh4jHx8jHx8jHyAhHyAhHSAiHSAjHR4jHh4jHx8jHx8hHyAhHyAiHSAjHSAjHR4jHh4jHx8hHx8hHyAhHyAiHyAjHSAjHR4jHR4hHh4hHx8hHyAiHyAjHyAjHSAjHR4jHR4hHh4hHx8hHyAjHyAjHyAjHSAjHR4hHR4hHR4hHx8iHyAjHyAjHyAjHSAhHR4hHR4hHR4hHx8jHyAjHyAjHyAjHyC9S2xeAAAA7nRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFxgZGhscHR4fICEiIyQlJicoKSorLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZISUpLTE1OUFFSU1RVVllaW1xdXmBhYmNkZWZnaGprbG1ub3Byc3R1dnd4eXp8fn+AgYKDhIWGiImKi4yNj5CRkpOUlZaXmJmam5ydnp+goaKjpKaoqqusra6vsLGys7S1tri5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+fkZpVQAABcBJREFUGBntwftjlQMcBvDnnLNL22qzJjWlKLHFVogyty3SiFq6EZliqZGyhnSxsLlMRahYoZKRFcul5dKFCatYqWZaNKvWtrPz/A2+7/b27qRzec/lPfvl/XxgMplMJpPJZDKZAtA9HJ3ppnIez0KnSdtC0RCNznHdJrbrh85wdSlVVRaEXuoGamYi5K5430HNiTiEWHKJg05eRWgNfKeV7RxbqUhGKPV/207VupQ8is0IoX5vtFC18SqEHaK4GyHTZ2kzVR8PBTCO4oANIZL4ShNVZcOhKKeYg9DoWdhI1ec3os2VFI0JCIUez5+i6st0qJZRrEAIJCw+QdW223BG/EmKwTBc/IJ/qfp2FDrkUnwFo8U9dZyqnaPhxLqfYjyM1S3vb6p+GGOBszsojoTDSDFz6qj66R4LzvYJxVMwUNRjf1H1ywQr/megg2RzLximy8waqvbda8M5iijegVEiHjlM1W/3h+FcXesphsMY4dMOUnUgOxyuPEzxPQwRNvV3qg5Nj4BreyimwADWe/dRVTMjEm6MoGLzGwtystL6RyOY3qSqdlYU3FpLZw1VW0sK5943MvUCKwJ1noNtjs6Ohge76Zq9ZkfpigU5WWkDYuCfbs1U5HWFR8/Qq4a9W0uK5k4ZmdrTCl8spGIePLPlbqqsc1Afe83O0hULc8alDYiBd7ZyitYMeBfR55rR2fOKP6ioPk2dGvZ+UVI0d8rtqT2tcCexlqK2F3wRn5Q+YVbBqrLKOupkr9lZujAOrmS0UpTb4JeIPkNHZ+cXr6uoPk2vyuBSPhWLEKj45PQJuQWryyqP0Z14uGLdROHIRNBEXDR09EP5r62rOHCazhrD4VKPwxTH+sIA3ZPTJ+YuWV22n+IruHFDC8X2CBjnPoolcGc2FYUwzmsUWXDHsoGKLBhmN0VvuBVfTVE/AAbpaid5CB4MbaLY1QXGuIViLTyZQcVyGGMuxWPwaA0Vk2GI9RRp8Ci2iuLkIBjhT5LNUfAspZFiTwyC72KK7+DNg1SsRvCNp3gZXq2k4iEEXSHFJHgVXUlxejCCbTvFAHiXdIJiXxyCK7KJ5FHoMZGK9xBcwyg2QpdlVMxEUM2iyIMuXXZQNF+HswxMsSAAJRQjoE//eoqDCXBSTO6f1xd+O0iyNRY6jaWi1ALNYCocZROj4JdEikroVkjFk9DcStXxpdfCD2MoXodu4RUU9ptxxmXssOfxnvDVcxRTod9FxyhqLoAqis5aPhwTDp9spRgEH2Q6KLbYoKqlaKTm6Isp0C/sJMnjFvhiERXPQvUNRe9p29lhR04CdBpC8Sl8YiuncIxEuzUUg4Dkgj+paVozygY9plPMh28SaymO9kabAopREGF3vt9MzeFFl8G7lRSZ8FFGK8XX4VA8QjEd7XrM3M0OXz8YCy+qKBLgq3wqnofiTorF0Ax56Rg1J1elW+BBAsVe+My6iYq7IK6keBdOIseV2qn5Pb8f3MqkWAXf9ThM8c8lAOIotuFsF875lRrH5klRcG0+xcPwQ1oLxfeRAP4heQTnGL78X2rqlw2DK59SXAV/zKaiGMAuko5InCt68mcOan5+ohf+z1pP8lQY/GHZQMV4YD3FpXDp4qerqbF/lBWBswyi+AL+ia+maLgcRRQj4IYlY/UpauqKBsPJAxQF8NM1TRQ/RudSPAD34rK3scOuR8/HGcspxsJfOVS8NZbiGXiUtPgINU3v3WFDmx8pEuG3EiqKKVbCC1vm2iZqap5LAtCtleQf8F9sFYWDohzeJczYyQ4V2bEZFGsQgJRGqqqhS2phHTWn9lDkIhBTqWqxQZ+IsRvtdHY9AvI2VX2hW68nfqGmuQsCEl3JdjfCF8OW1bPdtwhQ0gm2mQzfRE3a7KCYj0BNZJs8+Kxf/r6WtTEI2FIqlsMfFgRB5A6KUnSe/vUkX0AnuvUIt8SjM1m6wWQymUwmk8lkMgXRf5vi8rLQxtUhAAAAAElFTkSuQmCC`;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
let sections = state?.sections?.map((section) => ({
|
||||
properties: section.config.properties || {
|
||||
type: SectionType.CONTINUOUS,
|
||||
},
|
||||
headers: section.config.headers,
|
||||
footers: section.config.footers,
|
||||
children: section.children,
|
||||
}));
|
||||
let sections = state?.sections?.length
|
||||
? state.sections.map((section) => ({
|
||||
properties: section.config.properties || {
|
||||
type: SectionType.CONTINUOUS,
|
||||
},
|
||||
headers: section.config.headers,
|
||||
footers: section.config.footers,
|
||||
children: section.children,
|
||||
}))
|
||||
: undefined;
|
||||
if (!sections) {
|
||||
sections = [
|
||||
{
|
||||
|
||||
Generated
+82
-41
@@ -53,7 +53,7 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@braintree/sanitize-url':
|
||||
specifier: ^7.1.2
|
||||
specifier: 7.1.2
|
||||
version: 7.1.2
|
||||
'@casl/ability':
|
||||
specifier: 6.8.0
|
||||
@@ -62,7 +62,7 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:packages/editor-ext
|
||||
'@floating-ui/dom':
|
||||
specifier: ^1.7.3
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
'@hocuspocus/provider':
|
||||
specifier: 3.4.4
|
||||
@@ -74,10 +74,10 @@ importers:
|
||||
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)
|
||||
'@joplin/turndown':
|
||||
specifier: ^4.0.82
|
||||
specifier: 4.0.82
|
||||
version: 4.0.82
|
||||
'@joplin/turndown-plugin-gfm':
|
||||
specifier: ^1.0.64
|
||||
specifier: 1.0.64
|
||||
version: 1.0.64
|
||||
'@sindresorhus/slugify':
|
||||
specifier: 3.0.0
|
||||
@@ -170,34 +170,37 @@ importers:
|
||||
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)
|
||||
bytes:
|
||||
specifier: ^3.1.2
|
||||
specifier: 3.1.2
|
||||
version: 3.1.2
|
||||
cross-env:
|
||||
specifier: ^10.1.0
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
diff:
|
||||
specifier: 8.0.3
|
||||
version: 8.0.3
|
||||
docx:
|
||||
specifier: 9.7.1
|
||||
version: 9.7.1
|
||||
dompurify:
|
||||
specifier: 3.4.1
|
||||
version: 3.4.1
|
||||
fractional-indexing-jittered:
|
||||
specifier: ^1.0.0
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
highlight.js:
|
||||
specifier: ^11.11.1
|
||||
specifier: 11.11.1
|
||||
version: 11.11.1
|
||||
image-dimensions:
|
||||
specifier: ^2.5.0
|
||||
specifier: 2.5.0
|
||||
version: 2.5.0
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
specifier: 3.10.1
|
||||
version: 3.10.1
|
||||
linkifyjs:
|
||||
specifier: ^4.3.2
|
||||
specifier: 4.3.2
|
||||
version: 4.3.2
|
||||
marked:
|
||||
specifier: 17.0.5
|
||||
@@ -206,16 +209,16 @@ importers:
|
||||
specifier: 3.0.0-canary.1
|
||||
version: 3.0.0-canary.1
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4
|
||||
rfc6902:
|
||||
specifier: 5.2.0
|
||||
version: 5.2.0
|
||||
uuid:
|
||||
specifier: ^14.0.0
|
||||
specifier: 14.0.0
|
||||
version: 14.0.0
|
||||
y-indexeddb:
|
||||
specifier: ^9.0.12
|
||||
specifier: 9.0.12
|
||||
version: 9.0.12(yjs@13.6.30)
|
||||
y-prosemirror:
|
||||
specifier: 1.3.7
|
||||
@@ -228,16 +231,16 @@ importers:
|
||||
specifier: 22.6.1
|
||||
version: 22.6.1(@babel/traverse@7.28.5)(nx@22.6.1)
|
||||
'@types/bytes':
|
||||
specifier: ^3.1.5
|
||||
specifier: 3.1.5
|
||||
version: 3.1.5
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.6
|
||||
specifier: 1.5.6
|
||||
version: 1.5.6
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.6
|
||||
specifier: 5.0.6
|
||||
version: 5.0.6
|
||||
concurrently:
|
||||
specifier: ^9.2.1
|
||||
specifier: 9.2.1
|
||||
version: 9.2.1
|
||||
nx:
|
||||
specifier: 22.6.1
|
||||
@@ -484,13 +487,13 @@ importers:
|
||||
apps/server:
|
||||
dependencies:
|
||||
'@ai-sdk/google':
|
||||
specifier: ^3.0.52
|
||||
specifier: 3.0.52
|
||||
version: 3.0.52(zod@4.3.6)
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^3.0.47
|
||||
specifier: 3.0.47
|
||||
version: 3.0.47(zod@4.3.6)
|
||||
'@ai-sdk/openai-compatible':
|
||||
specifier: ^2.0.37
|
||||
specifier: 2.0.37
|
||||
version: 2.0.37(zod@4.3.6)
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: 3.1050.0
|
||||
@@ -505,7 +508,7 @@ importers:
|
||||
specifier: 12.31.0
|
||||
version: 12.31.0
|
||||
'@clickhouse/client':
|
||||
specifier: ^1.18.2
|
||||
specifier: 1.18.2
|
||||
version: 1.18.2
|
||||
'@docmost/pdf-inspector':
|
||||
specifier: 1.9.6
|
||||
@@ -589,43 +592,43 @@ importers:
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(socket.io-adapter@2.5.4)
|
||||
ai:
|
||||
specifier: ^6.0.134
|
||||
specifier: 6.0.134
|
||||
version: 6.0.134(zod@4.3.6)
|
||||
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)
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
bowser:
|
||||
specifier: ^2.14.1
|
||||
specifier: 2.14.1
|
||||
version: 2.14.1
|
||||
bullmq:
|
||||
specifier: ^5.76.10
|
||||
specifier: 5.76.10
|
||||
version: 5.76.10
|
||||
cache-manager:
|
||||
specifier: ^7.2.8
|
||||
specifier: 7.2.8
|
||||
version: 7.2.8
|
||||
cheerio:
|
||||
specifier: ^1.2.0
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0
|
||||
class-transformer:
|
||||
specifier: ^0.5.1
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1
|
||||
class-validator:
|
||||
specifier: ^0.15.1
|
||||
specifier: 0.15.1
|
||||
version: 0.15.1
|
||||
cookie:
|
||||
specifier: ^1.1.1
|
||||
specifier: 1.1.1
|
||||
version: 1.1.1
|
||||
fast-bm25:
|
||||
specifier: 0.0.5
|
||||
version: 0.0.5(typescript@5.9.3)
|
||||
fastify-ip:
|
||||
specifier: ^2.0.0
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
fs-extra:
|
||||
specifier: ^11.3.4
|
||||
specifier: 11.3.4
|
||||
version: 11.3.4
|
||||
happy-dom:
|
||||
specifier: 20.8.9
|
||||
@@ -736,13 +739,13 @@ importers:
|
||||
specifier: ^17.7.0
|
||||
version: 17.7.0
|
||||
tlds:
|
||||
specifier: ^1.261.0
|
||||
specifier: 1.261.0
|
||||
version: 1.261.0
|
||||
tmp-promise:
|
||||
specifier: ^3.0.3
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3
|
||||
tseep:
|
||||
specifier: ^1.3.1
|
||||
specifier: 1.3.1
|
||||
version: 1.3.1
|
||||
typesense:
|
||||
specifier: ^3.0.5
|
||||
@@ -6306,6 +6309,10 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
@@ -6977,6 +6984,9 @@ packages:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hash.js@1.1.7:
|
||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||
|
||||
hashery@1.4.0:
|
||||
resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -8111,6 +8121,9 @@ packages:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
minimatch@10.2.4:
|
||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
@@ -10264,6 +10277,10 @@ packages:
|
||||
xml-encryption@3.1.0:
|
||||
resolution: {integrity: sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==}
|
||||
|
||||
xml-js@1.6.11:
|
||||
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
|
||||
hasBin: true
|
||||
|
||||
xml-name-validator@5.0.0:
|
||||
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10276,6 +10293,9 @@ packages:
|
||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
xml@1.0.1:
|
||||
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
||||
|
||||
xmlbuilder@10.1.1:
|
||||
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
|
||||
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)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.4
|
||||
'@floating-ui/dom': 1.7.3
|
||||
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
|
||||
'@tiptap/pm': 3.20.4
|
||||
optional: true
|
||||
@@ -16620,6 +16640,15 @@ snapshots:
|
||||
dependencies:
|
||||
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.6.3: {}
|
||||
@@ -17541,6 +17570,11 @@ snapshots:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hash.js@1.1.7:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
hashery@1.4.0:
|
||||
dependencies:
|
||||
hookified: 1.15.1
|
||||
@@ -18821,6 +18855,8 @@ snapshots:
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minimalistic-assert@1.0.1: {}
|
||||
|
||||
minimatch@10.2.4:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.6
|
||||
@@ -20163,8 +20199,7 @@ snapshots:
|
||||
|
||||
sax@1.4.1: {}
|
||||
|
||||
sax@1.6.0:
|
||||
optional: true
|
||||
sax@1.6.0: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
@@ -21223,6 +21258,10 @@ snapshots:
|
||||
escape-html: 1.0.3
|
||||
xpath: 0.0.32
|
||||
|
||||
xml-js@1.6.11:
|
||||
dependencies:
|
||||
sax: 1.6.0
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xml-naming@0.1.0: {}
|
||||
@@ -21232,6 +21271,8 @@ snapshots:
|
||||
sax: 1.4.1
|
||||
xmlbuilder: 11.0.1
|
||||
|
||||
xml@1.0.1: {}
|
||||
|
||||
xmlbuilder@10.1.1: {}
|
||||
|
||||
xmlbuilder@11.0.1: {}
|
||||
|
||||
Reference in New Issue
Block a user