feat: export document

This commit is contained in:
fantasticit 2022-06-23 22:58:54 +08:00
parent b6c652baed
commit 20659d377c
20 changed files with 1246 additions and 5 deletions

View File

@ -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",

View File

@ -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

View File

@ -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);
}
}

View File

@ -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>
)}
</>
);
};

View File

@ -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;
}

View File

@ -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>
); );

View File

@ -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>
}
/>
);
};

View File

@ -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>
}
/>
);
};

View File

@ -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>
}
/>
);
};

View File

@ -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>
}
/>
);
};

View File

@ -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';

View File

@ -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]));
});
});
};

View File

@ -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';

View File

@ -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],
};
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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];

View File

@ -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;
}

View File

@ -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 };
} }

View File

@ -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'}