mirror of https://github.com/fantasticit/think.git
feat: export document
This commit is contained in:
parent
b6c652baed
commit
20659d377c
|
@ -55,11 +55,14 @@
|
||||||
"@tiptap/react": "^2.0.0-beta.107",
|
"@tiptap/react": "^2.0.0-beta.107",
|
||||||
"@tiptap/suggestion": "^2.0.0-beta.90",
|
"@tiptap/suggestion": "^2.0.0-beta.90",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
|
"buffer-image-size": "^0.6.4",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clone": "^2.1.2",
|
"clone": "^2.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"deep-equal": "^2.0.5",
|
"deep-equal": "^2.0.5",
|
||||||
|
"docx": "^7.3.0",
|
||||||
"dompurify": "^2.3.5",
|
"dompurify": "^2.3.5",
|
||||||
|
"downloadjs": "^1.4.7",
|
||||||
"interactjs": "^1.10.11",
|
"interactjs": "^1.10.11",
|
||||||
"katex": "^0.15.2",
|
"katex": "^0.15.2",
|
||||||
"kity": "^2.0.4",
|
"kity": "^2.0.4",
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { IconArticle, IconBranch, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons';
|
import { IconArticle, IconBranch, IconExport, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons';
|
||||||
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
|
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
|
||||||
import { ButtonProps } from '@douyinfe/semi-ui/button/Button';
|
import { ButtonProps } from '@douyinfe/semi-ui/button/Button';
|
||||||
|
import { IDocument } from '@think/domains';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { DocumentCreator } from 'components/document/create';
|
import { DocumentCreator } from 'components/document/create';
|
||||||
import { DocumentDeletor } from 'components/document/delete';
|
import { DocumentDeletor } from 'components/document/delete';
|
||||||
|
import { DocumentExporter } from 'components/document/export';
|
||||||
import { DocumentLinkCopyer } from 'components/document/link';
|
import { DocumentLinkCopyer } from 'components/document/link';
|
||||||
import { DocumentShare } from 'components/document/share';
|
import { DocumentShare } from 'components/document/share';
|
||||||
import { DocumentStar } from 'components/document/star';
|
import { DocumentStar } from 'components/document/star';
|
||||||
|
@ -17,6 +19,7 @@ import styles from './index.module.scss';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
wikiId: string;
|
wikiId: string;
|
||||||
documentId: string;
|
documentId: string;
|
||||||
|
document?: IDocument;
|
||||||
hoverVisible?: boolean;
|
hoverVisible?: boolean;
|
||||||
onStar?: () => void;
|
onStar?: () => void;
|
||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
|
@ -34,6 +37,7 @@ export const DocumentActions: React.FC<IProps> = ({
|
||||||
wikiId,
|
wikiId,
|
||||||
documentId,
|
documentId,
|
||||||
hoverVisible,
|
hoverVisible,
|
||||||
|
document,
|
||||||
onStar,
|
onStar,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
@ -179,6 +183,24 @@ export const DocumentActions: React.FC<IProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{document && (
|
||||||
|
<DocumentExporter
|
||||||
|
document={document}
|
||||||
|
render={({ toggleVisible }) => {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item onClick={() => toggleVisible(true)}>
|
||||||
|
<Text>
|
||||||
|
<Space>
|
||||||
|
<IconExport />
|
||||||
|
文档导出
|
||||||
|
</Space>
|
||||||
|
</Text>
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
|
|
||||||
<DocumentDeletor
|
<DocumentDeletor
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
.templateItem {
|
||||||
|
display: flex;
|
||||||
|
width: 118px;
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--semi-color-border);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { Badge, Button, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { IDocument } from '@think/domains';
|
||||||
|
import { IconJSON, IconMarkdown, IconPDF, IconWord } from 'components/icons';
|
||||||
|
import download from 'downloadjs';
|
||||||
|
import { safeJSONParse, safeJSONStringify } from 'helpers/json';
|
||||||
|
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { createEditor } from 'tiptap/core';
|
||||||
|
import { AllExtensions } from 'tiptap/core/all-kit';
|
||||||
|
import { prosemirrorToDocx } from 'tiptap/docx';
|
||||||
|
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import { printEditorContent } from './pdf';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
document: IDocument;
|
||||||
|
render?: (arg: { toggleVisible: (arg: boolean) => void }) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
||||||
|
const { isMobile } = IsOnMobile.useHook();
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
const editor = useMemo(() => {
|
||||||
|
return createEditor({
|
||||||
|
editable: false,
|
||||||
|
extensions: AllExtensions,
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportMarkdown = useCallback(() => {
|
||||||
|
const md = prosemirrorToMarkdown({ content: editor.state.doc.slice(0).content });
|
||||||
|
download(md, `${document.title}.md`, 'text/plain');
|
||||||
|
}, [document, editor]);
|
||||||
|
|
||||||
|
const exportJSON = useCallback(() => {
|
||||||
|
download(safeJSONStringify(editor.getJSON()), `${document.title}.json`, 'text/plain');
|
||||||
|
}, [document, editor]);
|
||||||
|
|
||||||
|
const exportWord = useCallback(() => {
|
||||||
|
prosemirrorToDocx(editor.view, editor.state).then((buffer) => {
|
||||||
|
download(buffer, `${document.title}.docx`);
|
||||||
|
});
|
||||||
|
}, [document, editor]);
|
||||||
|
|
||||||
|
const exportPDF = useCallback(() => {
|
||||||
|
printEditorContent(editor.view);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const content = useMemo(
|
||||||
|
() => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: '96vw',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '16px 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<div className={styles.templateItem} onClick={exportMarkdown}>
|
||||||
|
<header>
|
||||||
|
<IconMarkdown style={{ fontSize: 40 }} />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Text>Markdown</Text>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<Text type="tertiary">.md</Text>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.templateItem} onClick={exportJSON}>
|
||||||
|
<header>
|
||||||
|
<IconJSON style={{ fontSize: 40 }} />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Text>JSON</Text>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<Text type="tertiary">.json</Text>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.templateItem} onClick={exportWord}>
|
||||||
|
<header>
|
||||||
|
<Badge count="beta" type="danger">
|
||||||
|
<IconWord style={{ fontSize: 40 }} />
|
||||||
|
</Badge>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Text>Word</Text>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<Text type="tertiary">.docx</Text>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.templateItem} onClick={exportPDF}>
|
||||||
|
<header>
|
||||||
|
<Badge count="beta" type="danger">
|
||||||
|
<IconPDF style={{ fontSize: 40 }} />
|
||||||
|
</Badge>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Text>PDF</Text>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<Text type="tertiary">.pdf</Text>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[exportMarkdown, exportJSON, exportWord, exportPDF]
|
||||||
|
);
|
||||||
|
|
||||||
|
const btn = useMemo(
|
||||||
|
() =>
|
||||||
|
render ? (
|
||||||
|
render({ toggleVisible })
|
||||||
|
) : (
|
||||||
|
<Button type="primary" theme="light" onClick={toggleVisible}>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
[render, toggleVisible]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const c = safeJSONParse(document && document.content);
|
||||||
|
const json = c.default || c;
|
||||||
|
editor.commands.setContent(json);
|
||||||
|
}, [editor, document]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
title="文档导出"
|
||||||
|
visible={visible}
|
||||||
|
footer={null}
|
||||||
|
onCancel={toggleVisible}
|
||||||
|
style={{ maxWidth: '96vw' }}
|
||||||
|
zIndex={1061}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Modal>
|
||||||
|
{btn}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Dropdown
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={toggleVisible}
|
||||||
|
trigger="click"
|
||||||
|
position="bottomRight"
|
||||||
|
content={<div style={{ padding: '0 16px' }}>{content}</div>}
|
||||||
|
>
|
||||||
|
{btn}
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { EditorView } from 'prosemirror-view';
|
||||||
|
|
||||||
|
function printHtml(dom: Element) {
|
||||||
|
const style: string = Array.from(document.querySelectorAll('style, link')).reduce(
|
||||||
|
(str, style) => str + style.outerHTML,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
const content: string = style + dom.outerHTML;
|
||||||
|
|
||||||
|
const iframe: HTMLIFrameElement = document.createElement('iframe');
|
||||||
|
iframe.id = 'el-tiptap-iframe';
|
||||||
|
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;');
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
const frameWindow = iframe.contentWindow;
|
||||||
|
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
doc.open();
|
||||||
|
doc.write(content);
|
||||||
|
doc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameWindow) {
|
||||||
|
iframe.onload = function () {
|
||||||
|
try {
|
||||||
|
setTimeout(() => {
|
||||||
|
frameWindow.focus();
|
||||||
|
try {
|
||||||
|
if (!frameWindow.document.execCommand('print', false)) {
|
||||||
|
frameWindow.print();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
frameWindow.print();
|
||||||
|
}
|
||||||
|
frameWindow.close();
|
||||||
|
}, 10);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printEditorContent(view: EditorView) {
|
||||||
|
const editorContent = view.dom.closest('.ProseMirror');
|
||||||
|
if (editorContent) {
|
||||||
|
printHtml(editorContent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -74,7 +74,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
<Tooltip key="edit" content="编辑" position="bottom">
|
<Tooltip key="edit" content="编辑" position="bottom">
|
||||||
<Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} />
|
<Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{document && <DocumentActions wikiId={document.wikiId} documentId={documentId} />}
|
{document && <DocumentActions wikiId={document.wikiId} documentId={documentId} document={document} />}
|
||||||
<DocumentVersion documentId={documentId} />
|
<DocumentVersion documentId={documentId} />
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const IconJSON: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={style}
|
||||||
|
svg={
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
|
||||||
|
<path
|
||||||
|
d="M902.4 926.72c0 26.88-21.76 48.64-48.64 48.64H170.24c-26.88 0-48.64-21.76-48.64-48.64V48.64c0-26.88 21.76-48.64 48.64-48.64H588.8c12.8 0 25.6 5.12 34.56 14.08l263.68 263.68c8.96 8.96 14.08 21.76 14.08 34.56 1.28 0 1.28 614.4 1.28 614.4z"
|
||||||
|
fill="#ee9254"
|
||||||
|
p-id="13275"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M902.4 926.72v48.64c0 26.88-21.76 48.64-48.64 48.64H170.24c-26.88 0-48.64-21.76-48.64-48.64v-48.64c0 26.88 21.76 48.64 48.64 48.64h682.24c28.16 0 49.92-21.76 49.92-48.64z"
|
||||||
|
fill="#C1C7D0"
|
||||||
|
p-id="13276"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M24.32 536.32h975.36v243.2c0 26.88-21.76 48.64-48.64 48.64H72.96c-26.88 0-48.64-21.76-48.64-48.64v-243.2z"
|
||||||
|
fill="#FFAB00"
|
||||||
|
p-id="13277"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M121.6 536.32v-97.28l-97.28 97.28h97.28z m780.8 0l1.28-97.28 97.28 97.28h-98.56z"
|
||||||
|
fill="#FF8B00"
|
||||||
|
p-id="13278"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M902.4 312.32v7.68H637.44c-26.88 0-48.64-21.76-48.64-48.64V0c12.8 0 25.6 5.12 34.56 14.08l263.68 263.68c10.24 8.96 15.36 21.76 15.36 34.56z"
|
||||||
|
fill="#f8b87c"
|
||||||
|
p-id="13279"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M186.88 784.64c-29.44 0-47.36-12.8-60.16-30.72l21.76-21.76c11.52 14.08 21.76 21.76 38.4 21.76 17.92 0 29.44-11.52 29.44-37.12v-122.88h33.28v124.16c1.28 44.8-25.6 66.56-62.72 66.56zM368.64 672c39.68 10.24 60.16 24.32 60.16 55.04 0 35.84-28.16 56.32-66.56 56.32-28.16 0-56.32-10.24-78.08-30.72l20.48-23.04c17.92 15.36 35.84 24.32 58.88 24.32 20.48 0 33.28-8.96 33.28-24.32 0-14.08-7.68-20.48-42.24-29.44-39.68-10.24-62.72-21.76-62.72-56.32 0-33.28 26.88-55.04 64-55.04 26.88 0 48.64 8.96 67.84 23.04l-17.92 25.6c-16.64-12.8-33.28-19.2-51.2-19.2-19.2 0-30.72 10.24-30.72 23.04 0 16.64 8.96 23.04 44.8 30.72zM558.08 784.64c-57.6 0-98.56-43.52-98.56-97.28s40.96-97.28 98.56-97.28 98.56 43.52 98.56 97.28-40.96 97.28-98.56 97.28z m0-163.84c-37.12 0-64 29.44-64 66.56 0 37.12 26.88 66.56 64 66.56s64-29.44 64-66.56c0-35.84-26.88-66.56-64-66.56zM828.16 593.92h32v188.16H832l-102.4-134.4v134.4h-32V593.92h30.72l101.12 130.56V593.92z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
p-id="13280"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const IconMarkdown: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={style}
|
||||||
|
svg={
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="29352"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M970.24 431.104h-53.76V215.552L700.416 0H215.552a107.52 107.52 0 0 0-107.52 108.032v323.072H53.76A53.76 53.76 0 0 0 0 484.864v323.584a53.76 53.76 0 0 0 53.76 53.76h54.272v53.76A108.032 108.032 0 0 0 215.552 1024h592.896a108.032 108.032 0 0 0 107.52-108.032v-53.76h53.76a53.76 53.76 0 0 0 54.272-53.76V484.864a53.76 53.76 0 0 0-53.76-53.76zM161.792 102.4a53.76 53.76 0 0 1 53.76-55.808h431.104V153.6a108.032 108.032 0 0 0 108.032 108.032H862.72v162.304H161.792V102.4z m700.416 806.4a53.76 53.76 0 0 1-53.76 53.76H215.552a53.76 53.76 0 0 1-53.76-53.76v-53.76h700.416v53.76z"
|
||||||
|
fill="#1bb668"
|
||||||
|
p-id="29353"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M157 786.615V495.333h72.82l109.231 109.231 109.231-109.23h72.82v291.281h-72.82v-188.24l-109.23 109.23-109.231-109.23v188.24H157m509.744-291.282h109.23v145.641H867L721.359 804.821 575.718 640.974h91.026v-145.64z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
p-id="29354"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const IconPDF: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={style}
|
||||||
|
svg={
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
|
||||||
|
<path
|
||||||
|
d="M901.850593 926.476283a48.761858 48.761858 0 0 1-48.761859 48.761859H170.422718a48.761858 48.761858 0 0 1-48.761858-48.761859V48.762834a48.761858 48.761858 0 0 1 48.761858-48.761859h418.864363a48.761858 48.761858 0 0 1 34.620919 14.140939l263.801654 263.801654a48.761858 48.761858 0 0 1 14.140939 34.620919V926.476283z"
|
||||||
|
fill="#EBECF0"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M901.850593 926.476283v48.761859a48.761858 48.761858 0 0 1-48.761859 48.761858H170.422718a48.761858 48.761858 0 0 1-48.761858-48.761858v-48.761859a48.761858 48.761858 0 0 0 48.761858 48.761859h682.666016a48.761858 48.761858 0 0 0 48.761859-48.761859z"
|
||||||
|
fill="#C1C7D0"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M24.137143 536.381417h975.237166v243.809291a48.761858 48.761858 0 0 1-48.761858 48.761859H72.899001a48.761858 48.761858 0 0 1-48.761858-48.761859v-243.809291z"
|
||||||
|
fill="#FF5630"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M121.66086 536.381417V438.8577l-97.523717 97.523717h97.523717zM901.850593 536.381417l0.975237-97.523717 97.036098 97.523717H901.850593z"
|
||||||
|
fill="#DE350B"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M267.946434 585.143275h84.845634a57.051374 57.051374 0 0 1 41.935198 15.603795 55.1009 55.1009 0 0 1 16.091413 40.959961 55.588518 55.588518 0 0 1-16.091413 40.959961 59.001849 59.001849 0 0 1-43.398054 16.091413h-48.761858v76.556118H267.946434z m32.670446 81.919922h43.885672a42.422817 42.422817 0 0 0 25.843785-6.339041 23.893311 23.893311 0 0 0 7.801897-19.992362q0-24.868548-32.670445-24.868548h-44.860909zM434.71199 588.068987H511.755726a73.142787 73.142787 0 0 1 58.51423 25.356166 100.937047 100.937047 0 0 1 21.942836 68.266602 110.689418 110.689418 0 0 1-20.967599 69.729457A71.679932 71.679932 0 0 1 511.755726 780.190708H434.71199z m32.670445 158.963658H511.755726a43.398054 43.398054 0 0 0 36.083775-17.066651A75.093262 75.093262 0 0 0 560.517584 682.666992a70.704695 70.704695 0 0 0-13.65332-48.761859 48.761858 48.761858 0 0 0-37.546631-16.579031h-41.935198zM755.565018 618.788957h-100.937047v45.348529H755.565018v31.207589h-100.937047v81.919922h-32.670445v-190.171248H755.565018z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M901.850593 312.564487v6.82666h-263.801654a48.761858 48.761858 0 0 1-48.761858-48.761858V0.000975a48.761858 48.761858 0 0 1 34.620919 14.140939l264.289272 263.801654a48.761858 48.761858 0 0 1 13.653321 34.620919z"
|
||||||
|
fill="#C1C7D0"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const IconWord: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={style}
|
||||||
|
svg={
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
|
||||||
|
<path
|
||||||
|
d="M901.08928 925.3888a48.13824 48.13824 0 0 1-14.24896 34.38592 48.77312 48.77312 0 0 1-34.38592 14.24896H171.07456a48.13824 48.13824 0 0 1-34.3808-14.24896 48.74752 48.74752 0 0 1-14.24896-34.38592V49.42336a48.18944 48.18944 0 0 1 14.24896-34.40128A48.74752 48.74752 0 0 1 171.07456 0.77824h417.9968a48.5888 48.5888 0 0 1 34.59072 14.09024l263.3472 263.33184a48.68096 48.68096 0 0 1 14.08 34.59072z"
|
||||||
|
fill="#EBECF0"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M901.08928 925.3888v48.68608a48.18944 48.18944 0 0 1-14.24896 34.39616 48.77312 48.77312 0 0 1-34.38592 14.24896H171.07456a48.70656 48.70656 0 0 1-48.68096-48.69632v-48.63488a48.13824 48.13824 0 0 0 14.24384 34.38592 48.74752 48.74752 0 0 0 34.38592 14.24896h681.32864a48.81408 48.81408 0 0 0 48.68608-48.68608z"
|
||||||
|
fill="#C1C7D0"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M25.11872 536.09472h973.24544v243.33824a48.81408 48.81408 0 0 1-48.61952 48.68096H73.81504a48.18944 48.18944 0 0 1-34.39616-14.24384 48.74752 48.74752 0 0 1-14.24896-34.38592z"
|
||||||
|
fill="#317BFF"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M122.496 536.08448V438.71744L25.11872 536.08448z m778.59328 0l0.93184-97.36704 96.86016 97.36704z"
|
||||||
|
fill="#234AE8"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M901.08928 312.73984v6.84032h-263.3216a48.6912 48.6912 0 0 1-48.6912-48.68096V0.72704a48.5888 48.5888 0 0 1 34.5856 14.09024l263.76192 263.22944a49.3312 49.3312 0 0 1 13.66528 34.69312z"
|
||||||
|
fill="#C1C7D0"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M259.67616 606.58176l19.7888 102.54848 18.8928-102.5536h52.1728l-40.47872 165.52448H256.0768l-18.8928-96.25088h-0.90112l-18.88768 96.25088H164.32128l-41.37984-165.51936h53.07392l17.99168 102.54848h0.90112l19.7888-102.5536zM447.68256 602.0864q81.8432 3.60448 87.25504 86.35392-3.6096 82.77504-87.25504 86.35904-83.6608-2.69824-86.35904-87.26016 4.49024-80.95232 86.35904-85.4528z m0 40.47872q-30.59712 0.90624-32.384 45.8752 1.792 46.78656 32.384 47.67744 32.384-0.89088 33.28-47.67744-1.81248-45.8752-33.28-45.8752zM628.49024 714.53184h-16.18944V772.096h-53.96992v-165.51936h78.2592q76.4416-1.792 72.8576 46.77632-0.896 32.38912-27.88864 40.48384 30.5664 6.2976 28.78976 44.07296v7.19872q-0.90112 24.28416 5.4016 21.59104v5.39648h-54.88128q-2.68288-5.4016-2.688-28.78464 3.584-31.47776-29.69088-28.78464z m-16.18944-69.26848v32.384h21.59104q26.9824 0.90624 26.07616-15.29344-0.896-16.18944-22.4768-17.09056zM809.29792 772.096h-71.0656v-165.51424h72.86784q85.4528 0.90624 86.35904 79.16544 0 86.35392-88.16128 86.35392z m-17.08544-126.83264v88.15616h11.68896q38.66624 0 38.67648-44.9792 2.70336-46.76096-39.5776-43.17696z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -26,9 +26,11 @@ export * from './IconHeading3';
|
||||||
export * from './IconHorizontalRule';
|
export * from './IconHorizontalRule';
|
||||||
export * from './IconImage';
|
export * from './IconImage';
|
||||||
export * from './IconInfo';
|
export * from './IconInfo';
|
||||||
|
export * from './IconJSON';
|
||||||
export * from './IconLeft';
|
export * from './IconLeft';
|
||||||
export * from './IconLink';
|
export * from './IconLink';
|
||||||
export * from './IconList';
|
export * from './IconList';
|
||||||
|
export * from './IconMarkdown';
|
||||||
export * from './IconMath';
|
export * from './IconMath';
|
||||||
export * from './IconMergeCell';
|
export * from './IconMergeCell';
|
||||||
export * from './IconMessage';
|
export * from './IconMessage';
|
||||||
|
@ -40,6 +42,7 @@ export * from './IconMindRight';
|
||||||
export * from './IconMindSide';
|
export * from './IconMindSide';
|
||||||
export * from './IconOrderedList';
|
export * from './IconOrderedList';
|
||||||
export * from './IconOverview';
|
export * from './IconOverview';
|
||||||
|
export * from './IconPDF';
|
||||||
export * from './IconQuote';
|
export * from './IconQuote';
|
||||||
export * from './IconRight';
|
export * from './IconRight';
|
||||||
export * from './IconSearch';
|
export * from './IconSearch';
|
||||||
|
@ -57,5 +60,6 @@ export * from './IconTableHeaderColumn';
|
||||||
export * from './IconTableHeaderRow';
|
export * from './IconTableHeaderRow';
|
||||||
export * from './IconTableOfContents';
|
export * from './IconTableOfContents';
|
||||||
export * from './IconTask';
|
export * from './IconTask';
|
||||||
|
export * from './IconWord';
|
||||||
export * from './IconZoomIn';
|
export * from './IconZoomIn';
|
||||||
export * from './IconZoomOut';
|
export * from './IconZoomOut';
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { HeadingLevel } from 'docx';
|
||||||
|
import { EditorState } from 'prosemirror-state';
|
||||||
|
import { EditorView } from 'prosemirror-view';
|
||||||
|
import { Attachment } from 'tiptap/core/extensions/attachment';
|
||||||
|
import { BulletList } from 'tiptap/core/extensions/bullet-list';
|
||||||
|
import { Callout } from 'tiptap/core/extensions/callout';
|
||||||
|
import { CodeBlock } from 'tiptap/core/extensions/code-block';
|
||||||
|
import { DocumentChildren } from 'tiptap/core/extensions/document-children';
|
||||||
|
import { DocumentReference } from 'tiptap/core/extensions/document-reference';
|
||||||
|
import { Flow } from 'tiptap/core/extensions/flow';
|
||||||
|
import { HardBreak } from 'tiptap/core/extensions/hard-break';
|
||||||
|
import { HorizontalRule } from 'tiptap/core/extensions/horizontal-rule';
|
||||||
|
import { Iframe } from 'tiptap/core/extensions/iframe';
|
||||||
|
import { Katex } from 'tiptap/core/extensions/katex';
|
||||||
|
import { ListItem } from 'tiptap/core/extensions/listItem';
|
||||||
|
import { Mind } from 'tiptap/core/extensions/mind';
|
||||||
|
import { OrderedList } from 'tiptap/core/extensions/ordered-list';
|
||||||
|
import { Status } from 'tiptap/core/extensions/status';
|
||||||
|
import { TableOfContents } from 'tiptap/core/extensions/table-of-contents';
|
||||||
|
import { TaskItem } from 'tiptap/core/extensions/task-item';
|
||||||
|
import { TaskList } from 'tiptap/core/extensions/task-list';
|
||||||
|
import { Title } from 'tiptap/core/extensions/title';
|
||||||
|
|
||||||
|
import { defaultMarks, defaultNodes, DocxSerializer, writeDocx } from './prosemirror-docx';
|
||||||
|
|
||||||
|
function getLatexFromNode(node): string {
|
||||||
|
return node.attrs.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeSerializer = {
|
||||||
|
...defaultNodes,
|
||||||
|
[Title.name](state, node) {
|
||||||
|
state.renderInline(node);
|
||||||
|
state.closeBlock(node, { heading: HeadingLevel.TITLE });
|
||||||
|
},
|
||||||
|
[DocumentChildren.name](state, node) {
|
||||||
|
state.renderInline(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[DocumentReference.name](state, node) {
|
||||||
|
state.renderInline(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[TableOfContents.name](state, node) {
|
||||||
|
state.renderInline(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[BulletList.name](state, node) {
|
||||||
|
state.renderList(node, 'bullets');
|
||||||
|
},
|
||||||
|
[OrderedList.name](state, node) {
|
||||||
|
state.renderList(node, 'numbered');
|
||||||
|
},
|
||||||
|
[ListItem.name](state, node) {
|
||||||
|
state.renderListItem(node);
|
||||||
|
},
|
||||||
|
[HorizontalRule.name](state, node) {
|
||||||
|
state.closeBlock(node, { thematicBreak: true });
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[TaskList.name](state, node) {
|
||||||
|
state.renderInline(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[TaskItem.name](state, node) {
|
||||||
|
state.renderInline(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[CodeBlock.name](state, node) {
|
||||||
|
state.renderInline(node.content?.content ?? '');
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[Status.name](state, node) {
|
||||||
|
state.text(node.attrs.text ?? '');
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[Flow.name](state, node) {
|
||||||
|
state.renderContent(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[Mind.name](state, node) {
|
||||||
|
state.renderContent(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[HardBreak.name](state, node) {
|
||||||
|
state.addRunOptions({ break: 1 });
|
||||||
|
},
|
||||||
|
[Katex.name](state, node) {
|
||||||
|
state.math(getLatexFromNode(node), { inline: false });
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[Iframe.name](state, node) {
|
||||||
|
state.renderContent(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[Attachment.name](state, node) {
|
||||||
|
state.renderContent(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
[Callout.name](state, node) {
|
||||||
|
state.renderContent(node);
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const docxSerializer = new DocxSerializer(nodeSerializer, defaultMarks);
|
||||||
|
|
||||||
|
async function getImageBuffer(src: string) {
|
||||||
|
const image = await axios
|
||||||
|
.get(src, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return { data: '' };
|
||||||
|
});
|
||||||
|
return Buffer.from(image.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prosemirrorToDocx = async (view: EditorView, state: EditorState): Promise<Blob> => {
|
||||||
|
const dom = view.dom.closest('.ProseMirror');
|
||||||
|
const imageBufferCache = new Map();
|
||||||
|
const images = Array.from(await dom.querySelectorAll('img')) as HTMLImageElement[];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
images.map(async (img) => {
|
||||||
|
try {
|
||||||
|
const buffer = await getImageBuffer(img.src);
|
||||||
|
imageBufferCache.set(img.src, buffer);
|
||||||
|
} catch (e) {
|
||||||
|
imageBufferCache.set(img.src, Buffer.from('图片加载失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const wordDocument = docxSerializer.serialize(state.doc, {
|
||||||
|
getImageBuffer(src) {
|
||||||
|
return imageBufferCache.get(src);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
await writeDocx(wordDocument, (buffer) => {
|
||||||
|
imageBufferCache.clear();
|
||||||
|
resolve(new Blob([buffer]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { defaultDocxSerializer, defaultMarks, defaultNodes } from './schema';
|
||||||
|
export type { MarkSerializer, NodeSerializer } from './serializer';
|
||||||
|
export { DocxSerializer, DocxSerializerState } from './serializer';
|
||||||
|
export { createDocFromState, writeDocx } from './utils';
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { AlignmentType, convertInchesToTwip, ILevelsOptions, LevelFormat } from 'docx';
|
||||||
|
|
||||||
|
import { INumbering } from './types';
|
||||||
|
|
||||||
|
function basicIndentStyle(indent: number): Pick<ILevelsOptions, 'style' | 'alignment'> {
|
||||||
|
return {
|
||||||
|
alignment: AlignmentType.START,
|
||||||
|
style: {
|
||||||
|
paragraph: {
|
||||||
|
indent: { left: convertInchesToTwip(indent), hanging: convertInchesToTwip(0.18) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const numbered = Array(3)
|
||||||
|
.fill([LevelFormat.DECIMAL, LevelFormat.LOWER_LETTER, LevelFormat.LOWER_ROMAN])
|
||||||
|
.flat()
|
||||||
|
.map((format, level) => ({
|
||||||
|
level,
|
||||||
|
format,
|
||||||
|
text: `%${level + 1}.`,
|
||||||
|
...basicIndentStyle((level + 1) / 2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bullets = Array(3)
|
||||||
|
.fill(['●', '○', '■'])
|
||||||
|
.flat()
|
||||||
|
.map((text, level) => ({
|
||||||
|
level,
|
||||||
|
format: LevelFormat.BULLET,
|
||||||
|
text,
|
||||||
|
...basicIndentStyle((level + 1) / 2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
numbered,
|
||||||
|
bullets,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NumberingStyles = keyof typeof styles;
|
||||||
|
|
||||||
|
export function createNumbering(reference: string, style: NumberingStyles): INumbering {
|
||||||
|
return {
|
||||||
|
reference,
|
||||||
|
levels: styles[style],
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { HeadingLevel, ShadingType } from 'docx';
|
||||||
|
|
||||||
|
import { DocxSerializer, MarkSerializer, NodeSerializer } from './serializer';
|
||||||
|
import { getLatexFromNode } 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 const defaultMarks: MarkSerializer = {
|
||||||
|
em() {
|
||||||
|
return { italics: true };
|
||||||
|
},
|
||||||
|
strong() {
|
||||||
|
return { bold: true };
|
||||||
|
},
|
||||||
|
link() {
|
||||||
|
// Note, this is handled specifically in the serializer
|
||||||
|
// Word treats links more like a Node rather than a mark
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
code() {
|
||||||
|
return {
|
||||||
|
font: {
|
||||||
|
name: 'Monospace',
|
||||||
|
},
|
||||||
|
color: '000000',
|
||||||
|
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 };
|
||||||
|
},
|
||||||
|
underline() {
|
||||||
|
return {
|
||||||
|
underline: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
smallcaps() {
|
||||||
|
return { smallCaps: true };
|
||||||
|
},
|
||||||
|
allcaps() {
|
||||||
|
return { allCaps: true };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultDocxSerializer = new DocxSerializer(defaultNodes, defaultMarks);
|
|
@ -0,0 +1,376 @@
|
||||||
|
import sizeOf from 'buffer-image-size';
|
||||||
|
import {
|
||||||
|
AlignmentType,
|
||||||
|
Bookmark,
|
||||||
|
ExternalHyperlink,
|
||||||
|
FootnoteReferenceRun,
|
||||||
|
ImageRun,
|
||||||
|
InternalHyperlink,
|
||||||
|
IParagraphOptions,
|
||||||
|
IRunOptions,
|
||||||
|
ITableCellOptions,
|
||||||
|
Math,
|
||||||
|
MathRun,
|
||||||
|
Paragraph,
|
||||||
|
ParagraphChild,
|
||||||
|
SequentialIdentifier,
|
||||||
|
SimpleField,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
TabStopPosition,
|
||||||
|
TabStopType,
|
||||||
|
TextRun,
|
||||||
|
WidthType,
|
||||||
|
} from 'docx';
|
||||||
|
import { Mark, Node as ProsemirrorNode, Schema } from 'prosemirror-model';
|
||||||
|
|
||||||
|
import { createNumbering, NumberingStyles } from './numbering';
|
||||||
|
import { IFootnotes, INumbering, Mutable } from './types';
|
||||||
|
import { createDocFromState, createShortId } from './utils';
|
||||||
|
|
||||||
|
// This is duplicated from @curvenote/schema
|
||||||
|
export type AlignOptions = 'left' | 'center' | 'right';
|
||||||
|
|
||||||
|
export type NodeSerializer<S extends Schema = any> = Record<
|
||||||
|
string,
|
||||||
|
(state: DocxSerializerState<S>, node: ProsemirrorNode<S>, parent: ProsemirrorNode<S>, index: number) => void
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type MarkSerializer<S extends Schema = any> = Record<
|
||||||
|
string,
|
||||||
|
(state: DocxSerializerState<S>, node: ProsemirrorNode<S>, mark: Mark<S>) => IRunOptions
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
getImageBuffer: (src: string) => Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IMathOpts = {
|
||||||
|
inline?: boolean;
|
||||||
|
id?: string | null;
|
||||||
|
numbered?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_IMAGE_WIDTH = 600;
|
||||||
|
|
||||||
|
function createReferenceBookmark(id: string, kind: 'Equation' | 'Figure' | 'Table', before?: string, after?: string) {
|
||||||
|
const textBefore = before ? [new TextRun(before)] : [];
|
||||||
|
const textAfter = after ? [new TextRun(after)] : [];
|
||||||
|
return new Bookmark({
|
||||||
|
id,
|
||||||
|
children: [...textBefore, new SequentialIdentifier(kind), ...textAfter],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DocxSerializerState<S extends Schema = any> {
|
||||||
|
nodes: NodeSerializer<S>;
|
||||||
|
|
||||||
|
options: Options;
|
||||||
|
|
||||||
|
marks: MarkSerializer<S>;
|
||||||
|
|
||||||
|
children: (Paragraph | Table)[];
|
||||||
|
|
||||||
|
numbering: INumbering[];
|
||||||
|
|
||||||
|
footnotes: IFootnotes = {};
|
||||||
|
|
||||||
|
nextRunOpts?: IRunOptions;
|
||||||
|
|
||||||
|
current: ParagraphChild[] = [];
|
||||||
|
|
||||||
|
currentLink?: { link: string; children: IRunOptions[] };
|
||||||
|
|
||||||
|
// Optionally add options
|
||||||
|
nextParentParagraphOpts?: IParagraphOptions;
|
||||||
|
|
||||||
|
currentNumbering?: { reference: string; level: number };
|
||||||
|
|
||||||
|
constructor(nodes: NodeSerializer<S>, marks: MarkSerializer<S>, options: Options) {
|
||||||
|
this.nodes = nodes;
|
||||||
|
this.marks = marks;
|
||||||
|
// @ts-ignore
|
||||||
|
this.options = options ?? {};
|
||||||
|
this.children = [];
|
||||||
|
this.numbering = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent(parent: ProsemirrorNode<S>, opts?: IParagraphOptions) {
|
||||||
|
parent.forEach((node, _, i) => {
|
||||||
|
if (opts) this.addParagraphOptions(opts);
|
||||||
|
this.render(node, parent, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(node: ProsemirrorNode<S>, parent: ProsemirrorNode<S>, index: number) {
|
||||||
|
if (typeof parent === 'number') throw new Error('!');
|
||||||
|
if (!this.nodes[node.type.name]) throw new Error(`Token type \`${node.type.name}\` not supported by Word renderer`);
|
||||||
|
this.nodes[node.type.name](this, node, parent, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMarks(node: ProsemirrorNode<S>, marks: Mark[]): IRunOptions {
|
||||||
|
return marks
|
||||||
|
.map((mark) => {
|
||||||
|
return this.marks[mark.type.name]?.(this, node, mark);
|
||||||
|
})
|
||||||
|
.reduce((a, b) => ({ ...a, ...b }), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInline(parent: ProsemirrorNode<S>) {
|
||||||
|
// Pop the stack over to this object when we encounter a link, and closeLink restores it
|
||||||
|
let currentLink: { link: string; stack: ParagraphChild[] } | undefined;
|
||||||
|
const closeLink = () => {
|
||||||
|
if (!currentLink) return;
|
||||||
|
const hyperlink = new ExternalHyperlink({
|
||||||
|
link: currentLink.link,
|
||||||
|
// child: this.current[0],
|
||||||
|
children: this.current,
|
||||||
|
});
|
||||||
|
this.current = [...currentLink.stack, hyperlink];
|
||||||
|
currentLink = undefined;
|
||||||
|
};
|
||||||
|
const openLink = (href: string) => {
|
||||||
|
const sameLink = href === currentLink?.link;
|
||||||
|
this.addRunOptions({ style: 'Hyperlink' });
|
||||||
|
// TODO: https://github.com/dolanmiu/docx/issues/1119
|
||||||
|
// Remove the if statement here and oneLink!
|
||||||
|
const oneLink = true;
|
||||||
|
if (!oneLink) {
|
||||||
|
closeLink();
|
||||||
|
} else {
|
||||||
|
if (currentLink && sameLink) return;
|
||||||
|
if (currentLink && !sameLink) {
|
||||||
|
// Close previous, and open a new one
|
||||||
|
closeLink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLink = {
|
||||||
|
link: href,
|
||||||
|
stack: this.current,
|
||||||
|
};
|
||||||
|
this.current = [];
|
||||||
|
};
|
||||||
|
const progress = (node: ProsemirrorNode<S>, offset: number, index: number) => {
|
||||||
|
const links = node.marks.filter((m) => m.type.name === 'link');
|
||||||
|
const hasLink = links.length > 0;
|
||||||
|
if (hasLink) {
|
||||||
|
openLink(links[0].attrs.href);
|
||||||
|
} else if (!hasLink && currentLink) {
|
||||||
|
closeLink();
|
||||||
|
}
|
||||||
|
if (node.isText) {
|
||||||
|
this.text(node.text, this.renderMarks(node, node.marks));
|
||||||
|
} else {
|
||||||
|
this.render(node, parent, index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
parent.forEach(progress);
|
||||||
|
// Must call close at the end of everything, just in case
|
||||||
|
closeLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList(node: ProsemirrorNode<S>, style: NumberingStyles) {
|
||||||
|
if (!this.currentNumbering) {
|
||||||
|
const nextId = createShortId();
|
||||||
|
this.numbering.push(createNumbering(nextId, style));
|
||||||
|
this.currentNumbering = { reference: nextId, level: 0 };
|
||||||
|
} else {
|
||||||
|
const { reference, level } = this.currentNumbering;
|
||||||
|
this.currentNumbering = { reference, level: level + 1 };
|
||||||
|
}
|
||||||
|
this.renderContent(node);
|
||||||
|
if (this.currentNumbering.level === 0) {
|
||||||
|
delete this.currentNumbering;
|
||||||
|
} else {
|
||||||
|
const { reference, level } = this.currentNumbering;
|
||||||
|
this.currentNumbering = { reference, level: level - 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a pass through to the paragraphs, etc. underneath they will close the block
|
||||||
|
renderListItem(node: ProsemirrorNode<S>) {
|
||||||
|
if (!this.currentNumbering) throw new Error('Trying to create a list item without a list?');
|
||||||
|
this.addParagraphOptions({ numbering: this.currentNumbering });
|
||||||
|
this.renderContent(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
addParagraphOptions(opts: IParagraphOptions) {
|
||||||
|
this.nextParentParagraphOpts = { ...this.nextParentParagraphOpts, ...opts };
|
||||||
|
}
|
||||||
|
|
||||||
|
addRunOptions(opts: IRunOptions) {
|
||||||
|
this.nextRunOpts = { ...this.nextRunOpts, ...opts };
|
||||||
|
}
|
||||||
|
|
||||||
|
text(text: string | null | undefined, opts?: IRunOptions) {
|
||||||
|
if (!text) return;
|
||||||
|
this.current.push(new TextRun({ text, ...this.nextRunOpts, ...opts }));
|
||||||
|
delete this.nextRunOpts;
|
||||||
|
}
|
||||||
|
|
||||||
|
math(latex: string, opts: IMathOpts = { inline: true }) {
|
||||||
|
if (opts.inline || !opts.numbered) {
|
||||||
|
this.current.push(new Math({ children: [new MathRun(latex)] }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = opts.id ?? createShortId();
|
||||||
|
this.current = [
|
||||||
|
new TextRun('\t'),
|
||||||
|
new Math({
|
||||||
|
children: [new MathRun(latex)],
|
||||||
|
}),
|
||||||
|
new TextRun('\t('),
|
||||||
|
createReferenceBookmark(id, 'Equation'),
|
||||||
|
new TextRun(')'),
|
||||||
|
];
|
||||||
|
this.addParagraphOptions({
|
||||||
|
tabStops: [
|
||||||
|
{
|
||||||
|
type: TabStopType.CENTER,
|
||||||
|
position: TabStopPosition.MAX / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: TabStopType.RIGHT,
|
||||||
|
position: TabStopPosition.MAX,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// not sure what this actually is, seems to be close for 8.5x11
|
||||||
|
maxImageWidth = MAX_IMAGE_WIDTH;
|
||||||
|
|
||||||
|
image(src: string, widthPercent = 70, align: AlignOptions = 'center') {
|
||||||
|
const buffer = this.options.getImageBuffer(src);
|
||||||
|
|
||||||
|
if (!buffer) return;
|
||||||
|
|
||||||
|
const dimensions = sizeOf(buffer);
|
||||||
|
const aspect = dimensions.height / dimensions.width;
|
||||||
|
const width = this.maxImageWidth * (widthPercent / 100);
|
||||||
|
this.current.push(
|
||||||
|
new ImageRun({
|
||||||
|
data: buffer,
|
||||||
|
transformation: {
|
||||||
|
width,
|
||||||
|
height: width * aspect,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let alignment: AlignmentType;
|
||||||
|
switch (align) {
|
||||||
|
case 'right':
|
||||||
|
alignment = AlignmentType.RIGHT;
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
alignment = AlignmentType.LEFT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alignment = AlignmentType.CENTER;
|
||||||
|
}
|
||||||
|
this.addParagraphOptions({
|
||||||
|
alignment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
table(node: ProsemirrorNode<S>) {
|
||||||
|
const actualChildren = this.children;
|
||||||
|
const rows: TableRow[] = [];
|
||||||
|
node.content.forEach(({ content: rowContent }) => {
|
||||||
|
const cells: TableCell[] = [];
|
||||||
|
// Check if all cells are headers in this row
|
||||||
|
let tableHeader = true;
|
||||||
|
rowContent.forEach((cell) => {
|
||||||
|
if (cell.type.name !== 'table_header') {
|
||||||
|
tableHeader = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// This scales images inside of tables
|
||||||
|
this.maxImageWidth = MAX_IMAGE_WIDTH / rowContent.childCount;
|
||||||
|
rowContent.forEach((cell) => {
|
||||||
|
this.children = [];
|
||||||
|
this.renderContent(cell);
|
||||||
|
const tableCellOpts: Mutable<ITableCellOptions> = {
|
||||||
|
children: this.children,
|
||||||
|
};
|
||||||
|
const colspan = cell.attrs.colspan ?? 1;
|
||||||
|
const rowspan = cell.attrs.rowspan ?? 1;
|
||||||
|
if (colspan > 1) tableCellOpts.columnSpan = colspan;
|
||||||
|
if (rowspan > 1) tableCellOpts.rowSpan = rowspan;
|
||||||
|
cells.push(new TableCell(tableCellOpts));
|
||||||
|
});
|
||||||
|
rows.push(new TableRow({ children: cells, tableHeader }));
|
||||||
|
});
|
||||||
|
this.maxImageWidth = MAX_IMAGE_WIDTH;
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
rows,
|
||||||
|
// columnWidths: Array.from({ length: rows[0].cells.length }, () => 3505),
|
||||||
|
});
|
||||||
|
actualChildren.push(table);
|
||||||
|
// If there are multiple tables, this seperates them
|
||||||
|
actualChildren.push(new Paragraph(''));
|
||||||
|
this.children = actualChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
captionLabel(id: string, kind: 'Figure' | 'Table', { suffix } = { suffix: ': ' }) {
|
||||||
|
this.current.push(...[createReferenceBookmark(id, kind, `${kind} `), new TextRun(suffix)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$footnoteCounter = 0;
|
||||||
|
|
||||||
|
footnote(node: ProsemirrorNode<S>) {
|
||||||
|
const { current, nextRunOpts } = this;
|
||||||
|
// Delete everything and work with the footnote inline on the current
|
||||||
|
this.current = [];
|
||||||
|
delete this.nextRunOpts;
|
||||||
|
|
||||||
|
this.$footnoteCounter += 1;
|
||||||
|
this.renderInline(node);
|
||||||
|
this.footnotes[this.$footnoteCounter] = {
|
||||||
|
children: [new Paragraph({ children: this.current })],
|
||||||
|
};
|
||||||
|
this.current = current;
|
||||||
|
this.nextRunOpts = nextRunOpts;
|
||||||
|
this.current.push(new FootnoteReferenceRun(this.$footnoteCounter));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBlock(node: ProsemirrorNode<S>, props?: IParagraphOptions) {
|
||||||
|
const paragraph = new Paragraph({
|
||||||
|
children: this.current,
|
||||||
|
...this.nextParentParagraphOpts,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
this.current = [];
|
||||||
|
delete this.nextParentParagraphOpts;
|
||||||
|
this.children.push(paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
createReference(id: string, before?: string, after?: string) {
|
||||||
|
const children: ParagraphChild[] = [];
|
||||||
|
if (before) children.push(new TextRun(before));
|
||||||
|
children.push(new SimpleField(`REF ${id} \\h`));
|
||||||
|
if (after) children.push(new TextRun(after));
|
||||||
|
const ref = new InternalHyperlink({ anchor: id, children });
|
||||||
|
this.current.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DocxSerializer<S extends Schema = any> {
|
||||||
|
nodes: NodeSerializer<S>;
|
||||||
|
|
||||||
|
marks: MarkSerializer<S>;
|
||||||
|
|
||||||
|
constructor(nodes: NodeSerializer<S>, marks: MarkSerializer<S>) {
|
||||||
|
this.nodes = nodes;
|
||||||
|
this.marks = marks;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(content: ProsemirrorNode<S>, options: Options) {
|
||||||
|
const state = new DocxSerializerState<S>(this.nodes, this.marks, options);
|
||||||
|
state.renderContent(content);
|
||||||
|
const doc = createDocFromState(state);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { INumberingOptions } from 'docx';
|
||||||
|
import { IPropertiesOptions } from 'docx/build/file/core-properties';
|
||||||
|
|
||||||
|
export type Mutable<T> = {
|
||||||
|
-readonly [k in keyof T]: T[k];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IFootnotes = Mutable<Required<IPropertiesOptions>['footnotes']>;
|
||||||
|
export type INumbering = INumberingOptions['config'][0];
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Document, INumberingOptions, ISectionOptions, Packer, SectionType } from 'docx';
|
||||||
|
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
||||||
|
|
||||||
|
import { IFootnotes } from './types';
|
||||||
|
|
||||||
|
export function createShortId() {
|
||||||
|
return Math.random().toString(36).substr(2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDocFromState(state: {
|
||||||
|
numbering: INumberingOptions['config'];
|
||||||
|
children: ISectionOptions['children'];
|
||||||
|
footnotes?: IFootnotes;
|
||||||
|
}) {
|
||||||
|
const doc = new Document({
|
||||||
|
footnotes: state.footnotes,
|
||||||
|
numbering: {
|
||||||
|
config: state.numbering,
|
||||||
|
},
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
type: SectionType.CONTINUOUS,
|
||||||
|
},
|
||||||
|
children: state.children,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeDocx(
|
||||||
|
doc: Document,
|
||||||
|
write: ((buffer: Buffer) => void) | ((buffer: Buffer) => Promise<void>)
|
||||||
|
) {
|
||||||
|
const buffer = await Packer.toBuffer(doc);
|
||||||
|
return write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatexFromNode(node: ProsemirrorNode): string {
|
||||||
|
let math = '';
|
||||||
|
node.forEach((child) => {
|
||||||
|
if (child.isText) math += child.text;
|
||||||
|
// TODO: improve this as we may have other things in the future
|
||||||
|
});
|
||||||
|
return math;
|
||||||
|
}
|
|
@ -454,7 +454,7 @@ export class DocumentService {
|
||||||
}),
|
}),
|
||||||
this.viewService.getDocumentTotalViews(documentId),
|
this.viewService.getDocumentTotalViews(documentId),
|
||||||
]);
|
]);
|
||||||
const doc = lodash.omit(instanceToPlain(document), ['state', 'content']);
|
const doc = lodash.omit(instanceToPlain(document), ['state']);
|
||||||
const createUser = await this.userService.findById(doc.createUserId);
|
const createUser = await this.userService.findById(doc.createUserId);
|
||||||
return { document: { ...doc, views, createUser }, authority };
|
return { document: { ...doc, views, createUser }, authority };
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,12 +91,15 @@ importers:
|
||||||
'@typescript-eslint/eslint-plugin': ^5.21.0
|
'@typescript-eslint/eslint-plugin': ^5.21.0
|
||||||
'@typescript-eslint/parser': ^5.21.0
|
'@typescript-eslint/parser': ^5.21.0
|
||||||
axios: ^0.25.0
|
axios: ^0.25.0
|
||||||
|
buffer-image-size: ^0.6.4
|
||||||
classnames: ^2.3.1
|
classnames: ^2.3.1
|
||||||
clone: ^2.1.2
|
clone: ^2.1.2
|
||||||
copy-webpack-plugin: 11.0.0
|
copy-webpack-plugin: 11.0.0
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
deep-equal: ^2.0.5
|
deep-equal: ^2.0.5
|
||||||
|
docx: ^7.3.0
|
||||||
dompurify: ^2.3.5
|
dompurify: ^2.3.5
|
||||||
|
downloadjs: ^1.4.7
|
||||||
eslint: ^8.14.0
|
eslint: ^8.14.0
|
||||||
eslint-config-prettier: ^8.5.0
|
eslint-config-prettier: ^8.5.0
|
||||||
eslint-plugin-import: ^2.26.0
|
eslint-plugin-import: ^2.26.0
|
||||||
|
@ -194,11 +197,14 @@ importers:
|
||||||
'@tiptap/react': 2.0.0-beta.107_a3fcdb91535fe17b69dfabaa94f3bb3d
|
'@tiptap/react': 2.0.0-beta.107_a3fcdb91535fe17b69dfabaa94f3bb3d
|
||||||
'@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171
|
'@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171
|
||||||
axios: 0.25.0
|
axios: 0.25.0
|
||||||
|
buffer-image-size: 0.6.4
|
||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
clone: 2.1.2
|
clone: 2.1.2
|
||||||
cross-env: 7.0.3
|
cross-env: 7.0.3
|
||||||
deep-equal: 2.0.5
|
deep-equal: 2.0.5
|
||||||
|
docx: 7.3.0
|
||||||
dompurify: 2.3.5
|
dompurify: 2.3.5
|
||||||
|
downloadjs: 1.4.7
|
||||||
interactjs: 1.10.11
|
interactjs: 1.10.11
|
||||||
katex: 0.15.2
|
katex: 0.15.2
|
||||||
kity: 2.0.4
|
kity: 2.0.4
|
||||||
|
@ -4388,6 +4394,13 @@ packages:
|
||||||
/buffer-from/1.1.2:
|
/buffer-from/1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
/buffer-image-size/0.6.4:
|
||||||
|
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 17.0.35
|
||||||
|
dev: false
|
||||||
|
|
||||||
/buffer/5.7.1:
|
/buffer/5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5281,6 +5294,17 @@ packages:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/docx/7.3.0:
|
||||||
|
resolution: {integrity: sha512-OkSGlDNWMRFY07OEhUTx1ouuzYi8s1b67JDI6m5/5ek4xoshtP+/Rx8eRdY8LbhvpFkngvUantvTsxY4XW8Heg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 17.0.35
|
||||||
|
jszip: 3.10.0
|
||||||
|
nanoid: 3.3.1
|
||||||
|
xml: 1.0.1
|
||||||
|
xml-js: 1.6.11
|
||||||
|
dev: false
|
||||||
|
|
||||||
/domexception/2.0.1:
|
/domexception/2.0.1:
|
||||||
resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==}
|
resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -5313,6 +5337,10 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/downloadjs/1.4.7:
|
||||||
|
resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/duplexify/4.1.2:
|
/duplexify/4.1.2:
|
||||||
resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==}
|
resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6613,6 +6641,10 @@ packages:
|
||||||
resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
|
resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
/immediate/3.0.6:
|
||||||
|
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/import-fresh/3.3.0:
|
/import-fresh/3.3.0:
|
||||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -6994,7 +7026,7 @@ packages:
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/isarray/1.0.0:
|
/isarray/1.0.0:
|
||||||
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/isarray/2.0.5:
|
/isarray/2.0.5:
|
||||||
|
@ -7739,6 +7771,15 @@ packages:
|
||||||
object.assign: 4.1.2
|
object.assign: 4.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jszip/3.10.0:
|
||||||
|
resolution: {integrity: sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==}
|
||||||
|
dependencies:
|
||||||
|
lie: 3.3.0
|
||||||
|
pako: 1.0.11
|
||||||
|
readable-stream: 2.3.7
|
||||||
|
setimmediate: 1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jwa/1.4.1:
|
/jwa/1.4.1:
|
||||||
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7837,6 +7878,12 @@ packages:
|
||||||
resolution: {integrity: sha512-QqTX4UVsGy24njtCgLRspiKpxfRniRBZE/P+d0vQXuYWQ+hwDS6X0ouo0O/SRyf7bhhMCE71b6vAvLMtY5PfEw==}
|
resolution: {integrity: sha512-QqTX4UVsGy24njtCgLRspiKpxfRniRBZE/P+d0vQXuYWQ+hwDS6X0ouo0O/SRyf7bhhMCE71b6vAvLMtY5PfEw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lie/3.3.0:
|
||||||
|
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||||
|
dependencies:
|
||||||
|
immediate: 3.0.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lilconfig/2.0.4:
|
/lilconfig/2.0.4:
|
||||||
resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==}
|
resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -8820,6 +8867,10 @@ packages:
|
||||||
netmask: 2.0.2
|
netmask: 2.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/pako/1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/parent-module/1.0.1:
|
/parent-module/1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -10141,6 +10192,10 @@ packages:
|
||||||
send: 0.17.2
|
send: 0.17.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/setimmediate/1.0.5:
|
||||||
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/setprototypeof/1.2.0:
|
/setprototypeof/1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -11452,7 +11507,7 @@ packages:
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/util-deprecate/1.0.2:
|
/util-deprecate/1.0.2:
|
||||||
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
/utility-types/3.10.0:
|
/utility-types/3.10.0:
|
||||||
resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==}
|
resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==}
|
||||||
|
@ -11977,10 +12032,21 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/xml-js/1.6.11:
|
||||||
|
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
sax: 1.2.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xml-name-validator/3.0.0:
|
/xml-name-validator/3.0.0:
|
||||||
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
|
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/xml/1.0.1:
|
||||||
|
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xml2js/0.4.23:
|
/xml2js/0.4.23:
|
||||||
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
|
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
Loading…
Reference in New Issue