feat: export document to docx

This commit is contained in:
fantasticit 2022-08-10 12:37:17 +08:00
parent 3c11bfc121
commit 123d93fd07
18 changed files with 310 additions and 765 deletions

View File

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

View File

@ -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<IProps> = ({ 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<IProps> = ({ 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);

View File

@ -199,7 +199,19 @@ export const useDocumentDetail = (documentId, options: UseQueryOptions<IDocument
[refetch, documentId]
);
return { data, loading: isLoading, error, update, toggleStatus };
/**
*
*/
const exportDocx = useCallback(async (content) => {
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 };
};
/**

View File

@ -47,13 +47,13 @@ export const Title = Node.create<TitleOptions>({
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() {

View File

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

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

View File

@ -1,48 +0,0 @@
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

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

View File

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

@ -1,9 +0,0 @@
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

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

@ -32,6 +32,14 @@ export declare const DocumentApiDefinition: {
server: "detail/:id";
client: (id: IDocument['id']) => string;
};
/**
*
*/
exportDocx: {
method: "post";
server: "/export/docx";
client: () => string;
};
/**
*
*/

View File

@ -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'; }
},
/**
*
*/

View File

@ -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',
},
/**
*
*/

View File

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

View File

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

View File

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

View File

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