From 123d93fd075924717ebb53a7838e43efcd45260d Mon Sep 17 00:00:00 2001 From: fantasticit Date: Wed, 10 Aug 2022 12:37:17 +0800 Subject: [PATCH] feat: export document to docx --- packages/client/package.json | 1 + .../src/components/document/export/index.tsx | 14 +- packages/client/src/data/document.ts | 14 +- .../src/tiptap/core/extensions/title.ts | 4 +- packages/client/src/tiptap/docx/index.ts | 149 ------- .../src/tiptap/docx/prosemirror-docx/index.ts | 4 - .../tiptap/docx/prosemirror-docx/numbering.ts | 48 --- .../tiptap/docx/prosemirror-docx/schema.ts | 123 ------ .../docx/prosemirror-docx/serializer.ts | 376 ------------------ .../src/tiptap/docx/prosemirror-docx/types.ts | 9 - .../src/tiptap/docx/prosemirror-docx/utils.ts | 47 --- packages/domains/lib/api/document.d.ts | 8 + packages/domains/lib/api/document.js | 8 + packages/domains/src/api/document.ts | 9 + packages/server/package.json | 1 + .../src/controllers/document.controller.ts | 14 + .../server/src/services/document.service.ts | 17 + pnpm-lock.yaml | 229 ++++++++++- 18 files changed, 310 insertions(+), 765 deletions(-) delete mode 100644 packages/client/src/tiptap/docx/index.ts delete mode 100644 packages/client/src/tiptap/docx/prosemirror-docx/index.ts delete mode 100644 packages/client/src/tiptap/docx/prosemirror-docx/numbering.ts delete mode 100644 packages/client/src/tiptap/docx/prosemirror-docx/schema.ts delete mode 100644 packages/client/src/tiptap/docx/prosemirror-docx/serializer.ts delete mode 100644 packages/client/src/tiptap/docx/prosemirror-docx/types.ts delete mode 100644 packages/client/src/tiptap/docx/prosemirror-docx/utils.ts diff --git a/packages/client/package.json b/packages/client/package.json index a62cb42a..f4383f43 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -62,6 +62,7 @@ "docx": "^7.3.0", "dompurify": "^2.3.5", "downloadjs": "^1.4.7", + "html-to-docx": "^1.4.0", "interactjs": "^1.10.11", "katex": "^0.15.2", "kity": "^2.0.4", diff --git a/packages/client/src/components/document/export/index.tsx b/packages/client/src/components/document/export/index.tsx index a9e07b52..7ec11983 100644 --- a/packages/client/src/components/document/export/index.tsx +++ b/packages/client/src/components/document/export/index.tsx @@ -1,6 +1,7 @@ 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 { useDocumentDetail } from 'data/document'; import download from 'downloadjs'; import { safeJSONParse, safeJSONStringify } from 'helpers/json'; import { IsOnMobile } from 'hooks/use-on-mobile'; @@ -8,7 +9,6 @@ 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'; @@ -24,6 +24,7 @@ interface IProps { export const DocumentExporter: React.FC = ({ document, render }) => { const { isMobile } = IsOnMobile.useHook(); const [visible, toggleVisible] = useToggle(false); + const { exportDocx } = useDocumentDetail(document.id); const editor = useMemo(() => { return createEditor({ @@ -47,10 +48,13 @@ export const DocumentExporter: React.FC = ({ document, render }) => { }, [document, editor]); const exportWord = useCallback(() => { - prosemirrorToDocx(editor.view, editor.state).then((buffer) => { - download(buffer, `${document.title}.docx`); - }); - }, [document, editor]); + const editorContent = editor.view.dom.closest('.ProseMirror'); + if (editorContent) { + exportDocx(editorContent.outerHTML).then((res) => { + download(Buffer.from(res as Buffer), `${document.title}.docx`); + }); + } + }, [editor, exportDocx, document]); const exportPDF = useCallback(() => { printEditorContent(editor.view); diff --git a/packages/client/src/data/document.ts b/packages/client/src/data/document.ts index a8147370..d2172c50 100644 --- a/packages/client/src/data/document.ts +++ b/packages/client/src/data/document.ts @@ -199,7 +199,19 @@ export const useDocumentDetail = (documentId, options: UseQueryOptions { + const res = await HttpClient.request({ + method: DocumentApiDefinition.exportDocx.method, + url: DocumentApiDefinition.exportDocx.client(), + data: { content }, + }); + return res; + }, []); + + return { data, loading: isLoading, error, update, toggleStatus, exportDocx }; }; /** diff --git a/packages/client/src/tiptap/core/extensions/title.ts b/packages/client/src/tiptap/core/extensions/title.ts index b812c2ae..02c75b47 100644 --- a/packages/client/src/tiptap/core/extensions/title.ts +++ b/packages/client/src/tiptap/core/extensions/title.ts @@ -47,13 +47,13 @@ export const Title = Node.create({ parseHTML() { return [ { - tag: 'div[class=node-title]', + tag: 'h1[class=node-title]', }, ]; }, renderHTML({ HTMLAttributes }) { - return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + return ['h1', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; }, addNodeView() { diff --git a/packages/client/src/tiptap/docx/index.ts b/packages/client/src/tiptap/docx/index.ts deleted file mode 100644 index 73764bba..00000000 --- a/packages/client/src/tiptap/docx/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -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.renderContent(node); - 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 => { - 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])); - }); - }); -}; diff --git a/packages/client/src/tiptap/docx/prosemirror-docx/index.ts b/packages/client/src/tiptap/docx/prosemirror-docx/index.ts deleted file mode 100644 index 89eb1101..00000000 --- a/packages/client/src/tiptap/docx/prosemirror-docx/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { defaultDocxSerializer, defaultMarks, defaultNodes } from './schema'; -export type { MarkSerializer, NodeSerializer } from './serializer'; -export { DocxSerializer, DocxSerializerState } from './serializer'; -export { createDocFromState, writeDocx } from './utils'; diff --git a/packages/client/src/tiptap/docx/prosemirror-docx/numbering.ts b/packages/client/src/tiptap/docx/prosemirror-docx/numbering.ts deleted file mode 100644 index 42e6d440..00000000 --- a/packages/client/src/tiptap/docx/prosemirror-docx/numbering.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { AlignmentType, convertInchesToTwip, ILevelsOptions, LevelFormat } from 'docx'; - -import { INumbering } from './types'; - -function basicIndentStyle(indent: number): Pick { - 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], - }; -} diff --git a/packages/client/src/tiptap/docx/prosemirror-docx/schema.ts b/packages/client/src/tiptap/docx/prosemirror-docx/schema.ts deleted file mode 100644 index eb4a5353..00000000 --- a/packages/client/src/tiptap/docx/prosemirror-docx/schema.ts +++ /dev/null @@ -1,123 +0,0 @@ -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); diff --git a/packages/client/src/tiptap/docx/prosemirror-docx/serializer.ts b/packages/client/src/tiptap/docx/prosemirror-docx/serializer.ts deleted file mode 100644 index fc7347d5..00000000 --- a/packages/client/src/tiptap/docx/prosemirror-docx/serializer.ts +++ /dev/null @@ -1,376 +0,0 @@ -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 = Record< - string, - (state: DocxSerializerState, node: ProsemirrorNode, parent: ProsemirrorNode, index: number) => void ->; - -export type MarkSerializer = Record< - string, - (state: DocxSerializerState, node: ProsemirrorNode, mark: Mark) => 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 { - nodes: NodeSerializer; - - options: Options; - - marks: MarkSerializer; - - 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, marks: MarkSerializer, options: Options) { - this.nodes = nodes; - this.marks = marks; - // @ts-ignore - this.options = options ?? {}; - this.children = []; - this.numbering = []; - } - - renderContent(parent: ProsemirrorNode, opts?: IParagraphOptions) { - parent.forEach((node, _, i) => { - if (opts) this.addParagraphOptions(opts); - this.render(node, parent, i); - }); - } - - render(node: ProsemirrorNode, parent: ProsemirrorNode, 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, marks: Mark[]): IRunOptions { - return marks - .map((mark) => { - return this.marks[mark.type.name]?.(this, node, mark); - }) - .reduce((a, b) => ({ ...a, ...b }), {}); - } - - renderInline(parent: ProsemirrorNode) { - // 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, 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, 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) { - 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) { - 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 = { - 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) { - 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, 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 { - nodes: NodeSerializer; - - marks: MarkSerializer; - - constructor(nodes: NodeSerializer, marks: MarkSerializer) { - this.nodes = nodes; - this.marks = marks; - } - - serialize(content: ProsemirrorNode, options: Options) { - const state = new DocxSerializerState(this.nodes, this.marks, options); - state.renderContent(content); - const doc = createDocFromState(state); - return doc; - } -} diff --git a/packages/client/src/tiptap/docx/prosemirror-docx/types.ts b/packages/client/src/tiptap/docx/prosemirror-docx/types.ts deleted file mode 100644 index b53d955c..00000000 --- a/packages/client/src/tiptap/docx/prosemirror-docx/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { INumberingOptions } from 'docx'; -import { IPropertiesOptions } from 'docx/build/file/core-properties'; - -export type Mutable = { - -readonly [k in keyof T]: T[k]; -}; - -export type IFootnotes = Mutable['footnotes']>; -export type INumbering = INumberingOptions['config'][0]; diff --git a/packages/client/src/tiptap/docx/prosemirror-docx/utils.ts b/packages/client/src/tiptap/docx/prosemirror-docx/utils.ts deleted file mode 100644 index 530b104a..00000000 --- a/packages/client/src/tiptap/docx/prosemirror-docx/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -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) -) { - 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; -} diff --git a/packages/domains/lib/api/document.d.ts b/packages/domains/lib/api/document.d.ts index 2e5ce1ac..f0474612 100644 --- a/packages/domains/lib/api/document.d.ts +++ b/packages/domains/lib/api/document.d.ts @@ -32,6 +32,14 @@ export declare const DocumentApiDefinition: { server: "detail/:id"; client: (id: IDocument['id']) => string; }; + /** + * 导出文档 + */ + exportDocx: { + method: "post"; + server: "/export/docx"; + client: () => string; + }; /** * 更新文档 */ diff --git a/packages/domains/lib/api/document.js b/packages/domains/lib/api/document.js index 5a32ce70..930f44b5 100644 --- a/packages/domains/lib/api/document.js +++ b/packages/domains/lib/api/document.js @@ -34,6 +34,14 @@ exports.DocumentApiDefinition = { server: 'detail/:id', client: function (id) { return "/document/detail/".concat(id); } }, + /** + * 导出文档 + */ + exportDocx: { + method: 'post', + server: '/export/docx', + client: function () { return '/document/export/docx'; } + }, /** * 更新文档 */ diff --git a/packages/domains/src/api/document.ts b/packages/domains/src/api/document.ts index 8605b634..8a9fec6d 100644 --- a/packages/domains/src/api/document.ts +++ b/packages/domains/src/api/document.ts @@ -37,6 +37,15 @@ export const DocumentApiDefinition = { client: (id: IDocument['id']) => `/document/detail/${id}`, }, + /** + * 导出文档 + */ + exportDocx: { + method: 'post' as const, + server: '/export/docx' as const, + client: () => '/document/export/docx', + }, + /** * 更新文档 */ diff --git a/packages/server/package.json b/packages/server/package.json index a93c30e0..a9ea8c33 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,6 +45,7 @@ "express-rate-limit": "^6.2.0", "fs-extra": "^10.0.0", "helmet": "^5.0.2", + "html-to-docx": "^1.4.0", "ioredis": "^5.0.1", "lodash": "^4.17.21", "mysql2": "^2.3.3", diff --git a/packages/server/src/controllers/document.controller.ts b/packages/server/src/controllers/document.controller.ts index 1415a068..b72494a1 100644 --- a/packages/server/src/controllers/document.controller.ts +++ b/packages/server/src/controllers/document.controller.ts @@ -109,6 +109,20 @@ export class DocumentController { return await this.documentService.getDocumentVersion(req.user, documentId); } + /** + * 导出文档 + * @param req + * @param documentId + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(DocumentApiDefinition.exportDocx.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async exportDocx(@Body('content') content) { + return await this.documentService.exportDocx(content); + } + /** * 获取文档成员 * @param req diff --git a/packages/server/src/services/document.service.ts b/packages/server/src/services/document.service.ts index ebddecca..9e5f5abe 100644 --- a/packages/server/src/services/document.service.ts +++ b/packages/server/src/services/document.service.ts @@ -17,6 +17,7 @@ import { WikiService } from '@services/wiki.service'; import { EMPTY_DOCUMNENT } from '@think/constants'; import { AuthEnum, buildMessageURL, DocumentStatus, IUser } from '@think/domains'; import { instanceToPlain } from 'class-transformer'; +import * as HTMLtoDOCX from 'html-to-docx'; import * as lodash from 'lodash'; import { Repository } from 'typeorm'; @@ -761,4 +762,20 @@ export class DocumentService { .filter(Boolean) ); } + + /** + * 导出文档 + * html-to-docx + * @param user + * @param content + * @returns + */ + public async exportDocx(content) { + const fileBuffer = await HTMLtoDOCX(content, null, { + table: { row: { cantSplit: true } }, + footer: true, + pageNumber: true, + }); + return fileBuffer; + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea992e6e..29d890cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,7 @@ importers: eslint-plugin-react-hooks: ^4.5.0 eslint-plugin-simple-import-sort: ^7.0.0 fs-extra: ^10.0.0 + html-to-docx: ^1.4.0 interactjs: ^1.10.11 katex: ^0.15.2 kity: ^2.0.4 @@ -205,6 +206,7 @@ importers: docx: 7.3.0 dompurify: 2.3.5 downloadjs: 1.4.7 + html-to-docx: 1.4.0 interactjs: 1.10.11 katex: 0.15.2 kity: 2.0.4 @@ -326,6 +328,7 @@ importers: express-rate-limit: ^6.2.0 fs-extra: ^10.0.0 helmet: ^5.0.2 + html-to-docx: ^1.4.0 ioredis: ^5.0.1 jest: ^27.2.5 lodash: ^4.17.21 @@ -383,6 +386,7 @@ importers: express-rate-limit: 6.2.0_express@4.17.2 fs-extra: 10.0.0 helmet: 5.0.2 + html-to-docx: 1.4.0 ioredis: 5.0.1 lodash: 4.17.21 mysql2: 2.3.3 @@ -2615,6 +2619,57 @@ packages: transitivePeerDependencies: - encoding + /@oozcitak/dom/1.15.5: + resolution: {integrity: sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==} + engines: {node: '>=8.0'} + dependencies: + '@oozcitak/infra': 1.0.5 + '@oozcitak/url': 1.0.0 + '@oozcitak/util': 8.0.0 + dev: false + + /@oozcitak/infra/1.0.3: + resolution: {integrity: sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==} + engines: {node: '>=6.0'} + dependencies: + '@oozcitak/util': 1.0.1 + dev: false + + /@oozcitak/infra/1.0.5: + resolution: {integrity: sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==} + engines: {node: '>=6.0'} + dependencies: + '@oozcitak/util': 8.0.0 + dev: false + + /@oozcitak/url/1.0.0: + resolution: {integrity: sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==} + engines: {node: '>=8.0'} + dependencies: + '@oozcitak/infra': 1.0.3 + '@oozcitak/util': 1.0.2 + dev: false + + /@oozcitak/util/1.0.1: + resolution: {integrity: sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==} + engines: {node: '>=6.0'} + dev: false + + /@oozcitak/util/1.0.2: + resolution: {integrity: sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==} + engines: {node: '>=6.0'} + dev: false + + /@oozcitak/util/8.0.0: + resolution: {integrity: sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==} + engines: {node: '>=6.0'} + dev: false + + /@oozcitak/util/8.3.3: + resolution: {integrity: sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==} + engines: {node: '>=6.0'} + dev: false + /@popperjs/core/2.11.2: resolution: {integrity: sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==} dev: false @@ -4355,6 +4410,10 @@ packages: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} dev: true + /browser-split/0.0.1: + resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==} + dev: false + /browserslist/4.19.1: resolution: {integrity: sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4483,6 +4542,10 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + /camelize/1.0.0: + resolution: {integrity: sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg==} + dev: false + /caniuse-lite/1.0.30001304: resolution: {integrity: sha512-bdsfZd6K6ap87AGqSHJP/s1V+U6Z5lyrcbBu3ovbCCf8cSYpwTtGrCBObMpJqwxfTbLW6YTIdbb1jEeTelcpYQ==} @@ -5306,6 +5369,25 @@ packages: xml-js: 1.6.11 dev: false + /dom-serializer/0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + dependencies: + domelementtype: 2.3.0 + entities: 2.1.0 + dev: false + + /dom-walk/0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dev: false + + /domelementtype/1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + dev: false + + /domelementtype/2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + /domexception/2.0.1: resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} engines: {node: '>=8'} @@ -5313,10 +5395,23 @@ packages: webidl-conversions: 5.0.0 dev: true + /domhandler/2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + dependencies: + domelementtype: 1.3.1 + dev: false + /dompurify/2.3.5: resolution: {integrity: sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ==} dev: false + /domutils/1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + dev: false + /dot-prop/6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} @@ -5424,6 +5519,14 @@ packages: graceful-fs: 4.2.9 tapable: 2.2.1 + /ent/2.2.0: + resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} + dev: false + + /entities/1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + dev: false + /entities/2.1.0: resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} dev: false @@ -5439,6 +5542,14 @@ packages: is-arrayish: 0.2.1 dev: true + /error/4.4.0: + resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==} + dependencies: + camelize: 1.0.0 + string-template: 0.2.1 + xtend: 4.0.2 + dev: false + /es-abstract/1.19.1: resolution: {integrity: sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==} engines: {node: '>= 0.4'} @@ -5526,7 +5637,7 @@ packages: engines: {node: '>=6'} /escape-html/1.0.3: - resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -5845,6 +5956,12 @@ packages: resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=} engines: {node: '>= 0.6'} + /ev-store/7.0.0: + resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==} + dependencies: + individual: 3.0.0 + dev: false + /events/3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6416,6 +6533,13 @@ packages: which: 1.3.1 dev: true + /global/4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + dependencies: + min-document: 2.19.0 + process: 0.11.10 + dev: false + /globals/11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -6563,6 +6687,38 @@ packages: engines: {node: '>=8'} dev: true + /html-to-docx/1.4.0: + resolution: {integrity: sha512-VEbwMb5lwQZiJcnRZLofhziNtQ9UbVE7MQGICZKp896gC6P0ikfU6XRc8g4Z5qmYRBqLBXnY7b6iFYJ72yqNrw==} + dependencies: + color-name: 1.1.4 + escape-html: 1.0.3 + html-to-vdom: 0.7.0 + image-size: 1.0.2 + jszip: 3.10.0 + lodash: 4.17.21 + nanoid: 3.3.1 + virtual-dom: 2.1.1 + xmlbuilder2: 2.1.2 + dev: false + + /html-to-vdom/0.7.0: + resolution: {integrity: sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==} + dependencies: + ent: 2.2.0 + htmlparser2: 3.10.1 + dev: false + + /htmlparser2/3.10.1: + resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + dependencies: + domelementtype: 1.3.1 + domhandler: 2.4.2 + domutils: 1.7.0 + entities: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + /http-errors/1.8.1: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} @@ -6665,6 +6821,14 @@ packages: resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} engines: {node: '>= 4'} + /image-size/1.0.2: + resolution: {integrity: sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + queue: 6.0.2 + dev: false + /immediate/3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} dev: false @@ -6701,6 +6865,10 @@ packages: engines: {node: '>=8'} dev: true + /individual/3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} + dev: false + /inflight/1.0.6: resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} dependencies: @@ -6918,6 +7086,10 @@ packages: engines: {node: '>=8'} dev: false + /is-object/1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + dev: false + /is-path-cwd/2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} @@ -8319,6 +8491,12 @@ packages: engines: {node: '>=8'} dev: false + /min-document/2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + dependencies: + dom-walk: 0.1.2 + dev: false + /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -8500,6 +8678,10 @@ packages: - webpack dev: false + /next-tick/0.2.2: + resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==} + dev: false + /next/12.1.0_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-s885kWvnIlxsUFHq9UGyIyLiuD0G3BUC/xrH0CEnH5lHEWkwQcHOORgbDF0hbrW9vr/7am4ETfX4A7M6DjrE7Q==} engines: {node: '>=12.22.0'} @@ -9264,6 +9446,11 @@ packages: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} dev: false + /process/0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + /prompts/2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -9466,6 +9653,12 @@ packages: /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /queue/6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + dependencies: + inherits: 2.0.4 + dev: false + /quick-format-unescaped/4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false @@ -10471,6 +10664,10 @@ packages: strip-ansi: 6.0.1 dev: true + /string-template/0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + dev: false + /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -11558,6 +11755,19 @@ packages: resolution: {integrity: sha512-CjMt64yC9D+XUx2t3F0TPbh/Yt5+/ke8/s3IizXa6NtksdJUFDoCcNxi/KRZ9eiZPR/D77pHnnQzAtCoLDaGIw==} dev: false + /virtual-dom/2.1.1: + resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==} + dependencies: + browser-split: 0.0.1 + error: 4.4.0 + ev-store: 7.0.0 + global: 4.4.0 + is-object: 1.0.2 + next-tick: 0.2.2 + x-is-array: 0.1.0 + x-is-string: 0.1.0 + dev: false + /vm2/3.9.5: resolution: {integrity: sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==} engines: {node: '>=6.0'} @@ -11995,6 +12205,14 @@ packages: optional: true dev: false + /x-is-array/0.1.0: + resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==} + dev: false + + /x-is-string/0.1.0: + resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==} + dev: false + /xml-js/1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true @@ -12023,6 +12241,15 @@ packages: engines: {node: '>=4.0'} dev: false + /xmlbuilder2/2.1.2: + resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==} + engines: {node: '>=8.0'} + dependencies: + '@oozcitak/dom': 1.15.5 + '@oozcitak/infra': 1.0.5 + '@oozcitak/util': 8.3.3 + dev: false + /xmlchars/2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: true