♻️(frontend) export to docx

We can now export the document to docx format.
We adapted the frontend to be able to choose
between pdf or docx export.
This commit is contained in:
Anthony LC
2024-08-07 14:45:02 +02:00
committed by Anthony LC
parent 4280f0779e
commit c1566d98fe
10 changed files with 748 additions and 340 deletions

View File

@@ -12,6 +12,7 @@ and this project adheres to
- 🎨(frontend) better conversion editor to pdf #151
- ✨(frontend) Versioning #147
- ✨Export docx (word) #161
## Fixed

View File

@@ -0,0 +1,240 @@
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import jsdom from 'jsdom';
import pdf from 'pdf-parse';
import { createDoc } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Export', () => {
test('it converts the doc to pdf with a template integrated', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Export',
})
.click();
await page
.getByRole('button', {
name: 'Download',
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfText = (await pdf(pdfBuffer)).text;
expect(pdfText).toContain('Hello World'); // This is the doc text
});
test('it converts the doc to docx with a template integrated', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.docx`);
});
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Export',
})
.click();
await page.getByText('Docx').click();
await page
.getByRole('button', {
name: 'Download',
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
});
test('it converts the blocknote json in correct html for the export', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
let body = '';
await page.route('**/templates/*/generate-document/', async (route) => {
const request = route.request();
body = request.postDataJSON().body;
await route.continue();
});
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.locator('.bn-block-outer').last().fill('Hello World');
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('Break');
await expect(page.getByText('Break')).toBeVisible();
// Center the text
await page.getByText('Break').dblclick();
await page.locator('button[data-test="alignTextCenter"]').click();
// Change the background color
await page.getByText('Break').dblclick();
await page.locator('button[data-test="colors"]').click();
await page.locator('button[data-test="background-color-brown"]').click();
// Change the text color
await page.getByText('Break').dblclick();
await page.locator('button[data-test="colors"]').click();
await page.locator('button[data-test="text-color-orange"]').click();
// Add a list
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('Test List 1');
await page.getByText('Test List 1').dblclick();
await page
.getByRole('button', {
name: 'Paragraph',
})
.click();
await page
.getByRole('menuitem', {
name: 'Bullet List',
})
.click();
await page
.locator('.bn-block-content[data-content-type="bulletListItem"]')
.last()
.click();
await page.keyboard.press('Enter');
await page
.locator('.bn-block-content[data-content-type="bulletListItem"] p')
.last()
.fill('Test List 2');
await page.keyboard.press('Enter');
await page
.locator('.bn-block-content[data-content-type="bulletListItem"] p')
.last()
.fill('Test List 3');
// Add a number list
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('Test Number 1');
await page.getByText('Test Number 1').dblclick();
await page
.getByRole('button', {
name: 'Paragraph',
})
.click();
await page
.getByRole('menuitem', {
name: 'Numbered List',
})
.click();
await page
.locator('.bn-block-content[data-content-type="numberedListItem"]')
.last()
.click();
await page.keyboard.press('Enter');
await page
.locator('.bn-block-content[data-content-type="numberedListItem"] p')
.last()
.fill('Test Number 2');
await page.keyboard.press('Enter');
await page
.locator('.bn-block-content[data-content-type="numberedListItem"] p')
.last()
.fill('Test Number 3');
// Add img
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await page
.getByRole('option', {
name: 'Image',
})
.click();
await page
.getByPlaceholder('Enter URL')
.fill('https://example.com/image.jpg');
await page
.getByRole('button', {
name: 'Embed image',
})
.click();
// Download
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Export',
})
.click();
await page
.getByRole('button', {
name: 'Download',
})
.click();
// Empty paragraph should be replaced by a <br/>
expect(body.match(/<br>/g)?.length).toBeGreaterThanOrEqual(2);
expect(body).toContain('style="color: orange;"');
expect(body).toContain('custom-style="center"');
expect(body).toContain('style="background-color: brown;"');
const { JSDOM } = jsdom;
const DOMParser = new JSDOM().window.DOMParser;
const parser = new DOMParser();
const html = parser.parseFromString(body, 'text/html');
const ulLis = html.querySelectorAll('ul li');
expect(ulLis.length).toBe(3);
expect(ulLis[0].textContent).toBe('Test List 1');
expect(ulLis[1].textContent).toBe('Test List 2');
expect(ulLis[2].textContent).toBe('Test List 3');
const olLis = html.querySelectorAll('ol li');
expect(olLis.length).toBe(3);
expect(olLis[0].textContent).toBe('Test Number 1');
expect(olLis[1].textContent).toBe('Test Number 2');
expect(olLis[2].textContent).toBe('Test Number 3');
const img = html.querySelectorAll('img');
expect(img.length).toBe(1);
expect(img[0].src).toBe('https://example.com/image.jpg');
});
});

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { goToGridDoc, mockedDocument } from './common';
import { createDoc, goToGridDoc, mockedDocument } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -63,4 +63,194 @@ test.describe('Doc Header', () => {
await expect(card.getByText('Your role: Owner')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
});
test('it updates the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(
page,
'doc-update',
browserName,
1,
true,
);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Update document',
})
.click();
await expect(
page.locator('h2').getByText(`Update document "${randomDoc}"`),
).toBeVisible();
await expect(
page.getByRole('checkbox', { name: 'Is it public ?' }),
).toBeChecked();
await page.getByText('Document name').fill(`${randomDoc}-updated`);
await page.getByText('Is it public ?').click();
await page
.getByRole('button', {
name: 'Validate the modification',
})
.click();
await expect(
page.getByText('The document has been updated.'),
).toBeVisible();
const docTitle = await goToGridDoc(page, {
title: `${randomDoc}-updated`,
});
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Update document',
})
.click();
await expect(
page.getByRole('checkbox', { name: 'Is it public ?' }),
).not.toBeChecked();
});
test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Delete document',
})
.click();
await expect(
page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
).toBeVisible();
await page
.getByRole('button', {
name: 'Confirm deletion',
})
.click();
await expect(
page.getByText('The document has been deleted.'),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a new document' }),
).toBeVisible();
const row = page
.getByLabel('Datagrid of the documents page 1')
.getByRole('table')
.getByRole('row')
.filter({
hasText: randomDoc,
});
expect(await row.count()).toBe(0);
});
test('it checks the options available if administrator', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: true, // Means admin
update: true,
partial_update: true,
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Update document' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Delete document' }),
).toBeHidden();
});
test('it checks the options available if editor', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: false, // Means not admin
update: true,
partial_update: true, // Means editor
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Update document' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Delete document' }),
).toBeHidden();
});
test('it checks the options available if reader', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
manage_accesses: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Update document' }),
).toBeHidden();
await expect(
page.getByRole('button', { name: 'Delete document' }),
).toBeHidden();
});
});

View File

@@ -1,301 +0,0 @@
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import pdf from 'pdf-parse';
import { createDoc, goToGridDoc, mockedDocument } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Tools', () => {
test('it converts the doc to pdf with a template integrated', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Generate PDF',
})
.click();
await page
.getByRole('button', {
name: 'Download',
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfText = (await pdf(pdfBuffer)).text;
expect(pdfText).toContain('Hello World'); // This is the doc text
});
test('it converts the blocknote json in correct html for the pdf', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
let body = '';
await page.route('**/templates/*/generate-document/', async (route) => {
const request = route.request();
body = request.postDataJSON().body;
await route.continue();
});
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.locator('.bn-block-outer').last().fill('Hello World');
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('Break');
await expect(page.getByText('Break')).toBeVisible();
// Center the text
await page.getByText('Break').dblclick();
await page.locator('button[data-test="alignTextCenter"]').click();
// Change the background color
await page.getByText('Break').dblclick();
await page.locator('button[data-test="colors"]').click();
await page.locator('button[data-test="background-color-brown"]').click();
// Change the text color
await page.getByText('Break').dblclick();
await page.locator('button[data-test="colors"]').click();
await page.locator('button[data-test="text-color-orange"]').click();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Generate PDF',
})
.click();
await page
.getByRole('button', {
name: 'Download',
})
.click();
// Empty paragraph should be replaced by a <br/>
expect(body.match(/<br\/>/g)?.length).toBeGreaterThanOrEqual(2);
expect(body).toContain('style="color: orange;"');
expect(body).toContain('style="text-align: center;"');
expect(body).toContain('style="background-color: brown;"');
});
test('it updates the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(
page,
'doc-update',
browserName,
1,
true,
);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Update document',
})
.click();
await expect(
page.locator('h2').getByText(`Update document "${randomDoc}"`),
).toBeVisible();
await expect(
page.getByRole('checkbox', { name: 'Is it public ?' }),
).toBeChecked();
await page.getByText('Document name').fill(`${randomDoc}-updated`);
await page.getByText('Is it public ?').click();
await page
.getByRole('button', {
name: 'Validate the modification',
})
.click();
await expect(
page.getByText('The document has been updated.'),
).toBeVisible();
const docTitle = await goToGridDoc(page, {
title: `${randomDoc}-updated`,
});
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Update document',
})
.click();
await expect(
page.getByRole('checkbox', { name: 'Is it public ?' }),
).not.toBeChecked();
});
test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Delete document',
})
.click();
await expect(
page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
).toBeVisible();
await page
.getByRole('button', {
name: 'Confirm deletion',
})
.click();
await expect(
page.getByText('The document has been deleted.'),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a new document' }),
).toBeVisible();
const row = page
.getByLabel('Datagrid of the documents page 1')
.getByRole('table')
.getByRole('row')
.filter({
hasText: randomDoc,
});
expect(await row.count()).toBe(0);
});
test('it checks the options available if administrator', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: true, // Means admin
update: true,
partial_update: true,
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('button', { name: 'Generate PDF' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Update document' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Delete document' }),
).toBeHidden();
});
test('it checks the options available if editor', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: false, // Means not admin
update: true,
partial_update: true, // Means editor
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('button', { name: 'Generate PDF' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Update document' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Delete document' }),
).toBeHidden();
});
test('it checks the options available if reader', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
manage_accesses: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await expect(
page.getByRole('button', { name: 'Generate PDF' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Update document' }),
).toBeHidden();
await expect(
page.getByRole('button', { name: 'Delete document' }),
).toBeHidden();
});
});

View File

@@ -17,6 +17,7 @@
},
"dependencies": {
"convert-stream": "1.0.2",
"jsdom": "24.1.1",
"pdf-parse": "^1.1.1"
}
}

View File

@@ -2,17 +2,19 @@ import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
interface CreatePdfParams {
interface CreateExportParams {
templateId: string;
body: string;
body_type: 'html' | 'markdown';
format: 'pdf' | 'docx';
}
export const createPdf = async ({
export const createExport = async ({
templateId,
body,
body_type,
}: CreatePdfParams): Promise<Blob> => {
format,
}: CreateExportParams): Promise<Blob> => {
const response = await fetchAPI(
`templates/${templateId}/generate-document/`,
{
@@ -20,19 +22,23 @@ export const createPdf = async ({
body: JSON.stringify({
body,
body_type,
format,
}),
},
);
if (!response.ok) {
throw new APIError('Failed to create the pdf', await errorCauses(response));
throw new APIError(
'Failed to export the document',
await errorCauses(response),
);
}
return await response.blob();
};
export function useCreatePdf() {
return useMutation<Blob, APIError, CreatePdfParams>({
mutationFn: createPdf,
export function useExport() {
return useMutation<Blob, APIError, CreateExportParams>({
mutationFn: createExport,
});
}

View File

@@ -10,7 +10,7 @@ import {
ModalUpdateDoc,
} from '@/features/docs/doc-management';
import { ModalPDF } from './ModalPDF';
import { ModalPDF } from './ModalExport';
interface DocToolBoxProps {
doc: Doc;
@@ -83,10 +83,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">picture_as_pdf</span>}
icon={<span className="material-icons">file_download</span>}
size="small"
>
<Text $theme="primary">{t('Generate PDF')}</Text>
<Text $theme="primary">{t('Export')}</Text>
</Button>
</Box>
</DropButton>

View File

@@ -4,6 +4,8 @@ import {
Loader,
Modal,
ModalSize,
Radio,
RadioGroup,
Select,
VariantType,
useToastProvider,
@@ -15,7 +17,7 @@ import { Box, Text } from '@/components';
import { useDocStore } from '@/features/docs/doc-editor/';
import { Doc } from '@/features/docs/doc-management';
import { useCreatePdf } from '../api/useCreatePdf';
import { useExport } from '../api/useExport';
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
import { adaptBlockNoteHTML, downloadFile } from '../utils';
@@ -31,14 +33,14 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
const { toast } = useToastProvider();
const { docsStore } = useDocStore();
const {
mutate: createPdf,
data: pdf,
mutate: createExport,
data: documentGenerated,
isSuccess,
isPending,
error,
} = useCreatePdf();
} = useExport();
const [templateIdSelected, setTemplateIdSelected] = useState<string>();
const [format, setFormat] = useState<'pdf' | 'docx'>('pdf');
const templateOptions = useMemo(() => {
if (!templates?.pages) {
@@ -73,7 +75,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
}, [error, t]);
useEffect(() => {
if (!pdf || !isSuccess) {
if (!documentGenerated || !isSuccess) {
return;
}
@@ -84,16 +86,21 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s/g, '-');
downloadFile(pdf, `${title}.pdf`);
downloadFile(documentGenerated, `${title}.${format}`);
toast(t('Your pdf was downloaded succesfully'), VariantType.SUCCESS);
toast(
t('Your {{format}} was downloaded succesfully', {
format,
}),
VariantType.SUCCESS,
);
onClose();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pdf, isSuccess, t]);
}, [documentGenerated, isSuccess, t]);
async function onSubmit() {
if (!templateIdSelected) {
if (!templateIdSelected || !format) {
return;
}
@@ -107,10 +114,11 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
let body = await editor.blocksToFullHTML(editor.document);
body = adaptBlockNoteHTML(body);
createPdf({
createExport({
templateId: templateIdSelected,
body,
body_type: 'html',
format,
});
}
@@ -148,20 +156,20 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
picture_as_pdf
</Text>
<Text as="h2" $size="h3" $margin="none" $theme="primary">
{t('Generate PDF')}
{t('Export')}
</Text>
</Box>
}
>
<Box
$margin={{ bottom: 'xl' }}
aria-label={t('Content modal to generate a PDF')}
aria-label={t('Content modal to export the document')}
$gap="1.5rem"
>
<Alert canClose={false} type={VariantType.INFO}>
<Text>
{t(
'Generate a PDF from your document, it will be inserted in the selected template.',
'Export your document, it will be inserted in the selected template.',
)}
</Text>
</Alert>
@@ -176,6 +184,22 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
}
/>
<RadioGroup>
<Radio
label={t('PDF')}
value="pdf"
name="format"
onChange={(evt) => setFormat(evt.target.value as 'pdf')}
defaultChecked={true}
/>
<Radio
label={t('Docx')}
value="docx"
name="format"
onChange={(evt) => setFormat(evt.target.value as 'docx')}
/>
</RadioGroup>
{isPending && (
<Box $align="center" $margin={{ top: 'big' }}>
<Loader />

View File

@@ -10,16 +10,137 @@ export function downloadFile(blob: Blob, filename: string) {
window.URL.revokeObjectURL(url);
}
const convertToLi = (html: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const divs = doc.querySelectorAll(
'div[data-content-type="bulletListItem"] , div[data-content-type="numberedListItem"]',
);
// Loop through each div and replace it with a li
divs.forEach((div) => {
// Create a new li element
const li = document.createElement('li');
// Copy the attributes from the div to the li
for (let i = 0; i < div.attributes.length; i++) {
li.setAttribute(div.attributes[i].name, div.attributes[i].value);
}
// Move all child elements of the div to the li
while (div.firstChild) {
li.appendChild(div.firstChild);
}
// Replace the div with the li in the DOM
if (div.parentNode) {
div.parentNode.replaceChild(li, div);
}
});
/**
* Convert the blocknote content to a simplified version to be
* correctly parsed by our pdf and docx parser
*/
const newContent: string[] = [];
let currentList: HTMLUListElement | HTMLOListElement | null = null;
// Iterate over all the children of the bn-block-group
doc.body
.querySelectorAll('.bn-block-group .bn-block-outer')
.forEach((outerDiv) => {
const blockContent = outerDiv.querySelector('.bn-block-content');
if (blockContent) {
const contentType = blockContent.getAttribute('data-content-type');
if (contentType === 'bulletListItem') {
// If a list is not started, start a new one
if (!currentList) {
currentList = document.createElement('ul');
}
currentList.appendChild(blockContent);
} else if (contentType === 'numberedListItem') {
// If a numbered list is not started, start a new one
if (!currentList) {
currentList = document.createElement('ol');
}
currentList.appendChild(blockContent);
} else {
/***
* If there is a current list, add it to the new content
* It means that the current list has ended
*/
if (currentList) {
newContent.push(currentList.outerHTML);
}
currentList = null;
newContent.push(outerDiv.outerHTML);
}
} else {
// In case there is no content-type, add the outerDiv as is
newContent.push(outerDiv.outerHTML);
}
});
return newContent.join('');
};
const convertToImg = (html: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const divs = doc.querySelectorAll('div[data-content-type="image"]');
// Loop through each div and replace it with a img
divs.forEach((div) => {
const img = document.createElement('img');
// Copy the attributes from the div to the img
for (let i = 0; i < div.attributes.length; i++) {
img.setAttribute(div.attributes[i].name, div.attributes[i].value);
if (div.attributes[i].name === 'data-url') {
img.setAttribute('src', div.attributes[i].value);
}
if (div.attributes[i].name === 'data-preview-width') {
img.setAttribute('width', div.attributes[i].value);
}
}
// Move all child elements of the div to the img
while (div.firstChild) {
img.appendChild(div.firstChild);
}
// Replace the div with the img in the DOM
if (div.parentNode) {
div.parentNode.replaceChild(img, div);
}
});
return doc.body.innerHTML;
};
export const adaptBlockNoteHTML = (html: string) => {
html = html.replaceAll('<p class="bn-inline-content"></p>', '<br/>');
// custom-style is used by pandoc to convert the style
html = html.replaceAll(
/data-text-alignment=\"([a-z]+)\"/g,
'style="text-align: $1;"',
'custom-style="$1"',
);
html = html.replaceAll(/data-text-color=\"([a-z]+)\"/g, 'style="color: $1;"');
html = html.replaceAll(
/data-background-color=\"([a-z]+)\"/g,
'style="background-color: $1;"',
);
html = convertToLi(html);
html = convertToImg(html);
return html;
};

View File

@@ -1078,7 +1078,7 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blocknote/core@*", "@blocknote/core@0.15.3", "@blocknote/core@^0.15.3":
"@blocknote/core@*", "@blocknote/core@^0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.15.3.tgz#775ba61e1d61f0e3651988265c498bc1d573f25e"
integrity sha512-2ZOWpxt4rm5YdH9Gn9YXYQsI9wYMAkFcKkuSpprHGCl7ALk+Iv7Gw+BswttlhKFyYoY/df7dlc9lcZr5zVAWWw==
@@ -1125,7 +1125,7 @@
y-protocols "^1.0.6"
yjs "^13.6.15"
"@blocknote/mantine@*", "@blocknote/mantine@0.15.3":
"@blocknote/mantine@*":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.15.3.tgz#040b36df2eb262319850205362adc76be6282b22"
integrity sha512-KEtRffEAjqmIW9sSpDdiEgOhsSZLcI2zV/0hHOe/ZM/01UE/V4Vl7VKicgDd7FUBzn2BoqqxOv0GW1n5mLsfCw==
@@ -1139,7 +1139,7 @@
react-dom "^18"
react-icons "^5.2.1"
"@blocknote/react@*", "@blocknote/react@0.15.3", "@blocknote/react@^0.15.3":
"@blocknote/react@*", "@blocknote/react@^0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.15.3.tgz#e47486d453a223f5660297721748279cbc71e943"
integrity sha512-lawcNmXIo+dQkd7gZkCbvNmKLNL/3y82q5NT26VNQH0axQSTJ7/Mjiam6A82gCvcPWXx/1ILh6OClx8IV977ag==
@@ -4834,7 +4834,7 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
"@types/node@*", "@types/node@20.14.13":
"@types/node@*":
version "20.14.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.13.tgz#bf4fe8959ae1c43bc284de78bd6c01730933736b"
integrity sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==
@@ -4861,7 +4861,7 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/react-dom@*", "@types/react-dom@18.3.0":
"@types/react-dom@*":
version "18.3.0"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
@@ -5255,6 +5255,13 @@ agent-base@6:
dependencies:
debug "4"
agent-base@^7.0.2, agent-base@^7.1.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==
dependencies:
debug "^4.3.4"
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -6124,7 +6131,7 @@ crelt@^1.0.0:
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-env@*, cross-env@7.0.3:
cross-env@*:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
@@ -6230,6 +6237,13 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
cssstyle@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a"
integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==
dependencies:
rrweb-cssom "^0.6.0"
csstype@3.1.3, csstype@^3.0.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
@@ -6249,6 +6263,14 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0"
data-urls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==
dependencies:
whatwg-mimetype "^4.0.0"
whatwg-url "^14.0.0"
data-view-buffer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
@@ -6309,7 +6331,7 @@ debug@^4.0.0, debug@^4.3.6:
dependencies:
ms "2.1.2"
decimal.js@^10.4.2:
decimal.js@^10.4.2, decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
@@ -7903,6 +7925,13 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
html-encoding-sniffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==
dependencies:
whatwg-encoding "^3.1.1"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@@ -7949,6 +7978,14 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
dependencies:
agent-base "^7.1.0"
debug "^4.3.4"
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@@ -7957,6 +7994,14 @@ https-proxy-agent@^5.0.1:
agent-base "6"
debug "4"
https-proxy-agent@^7.0.5:
version "7.0.5"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2"
integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==
dependencies:
agent-base "^7.0.2"
debug "4"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@@ -8902,6 +8947,33 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
jsdom@24.1.1:
version "24.1.1"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.1.1.tgz#f41df8f4f3b2fbfa7e1bdc5df62c9804fd14a9d0"
integrity sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==
dependencies:
cssstyle "^4.0.1"
data-urls "^5.0.0"
decimal.js "^10.4.3"
form-data "^4.0.0"
html-encoding-sniffer "^4.0.0"
http-proxy-agent "^7.0.2"
https-proxy-agent "^7.0.5"
is-potential-custom-element-name "^1.0.1"
nwsapi "^2.2.12"
parse5 "^7.1.2"
rrweb-cssom "^0.7.1"
saxes "^6.0.0"
symbol-tree "^3.2.4"
tough-cookie "^4.1.4"
w3c-xmlserializer "^5.0.0"
webidl-conversions "^7.0.0"
whatwg-encoding "^3.1.1"
whatwg-mimetype "^4.0.0"
whatwg-url "^14.0.0"
ws "^8.18.0"
xml-name-validator "^5.0.0"
jsdom@^20.0.0:
version "20.0.3"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db"
@@ -9909,6 +9981,11 @@ nth-check@^2.0.1:
dependencies:
boolbase "^1.0.0"
nwsapi@^2.2.12:
version "2.2.12"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8"
integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==
nwsapi@^2.2.2:
version "2.2.10"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8"
@@ -10094,7 +10171,7 @@ parse5@^6.0.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@^7.0.0, parse5@^7.1.1:
parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
@@ -10505,7 +10582,7 @@ punycode.js@^2.3.1:
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@^2.1.0, punycode@^2.1.1:
punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@@ -11227,6 +11304,16 @@ rope-sequence@^1.3.0:
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
rrweb-cssom@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1"
integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==
rrweb-cssom@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b"
integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==
rsvp@^4.8.2:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -12008,7 +12095,7 @@ touch@^3.1.0:
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
tough-cookie@^4.1.2:
tough-cookie@^4.1.2, tough-cookie@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
@@ -12032,6 +12119,13 @@ tr46@^3.0.0:
dependencies:
punycode "^2.1.1"
tr46@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec"
integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==
dependencies:
punycode "^2.3.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -12199,7 +12293,7 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript@*, typescript@5.5.4, typescript@^5.0.4:
typescript@*, typescript@^5.0.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
@@ -12593,6 +12687,13 @@ w3c-xmlserializer@^4.0.0:
dependencies:
xml-name-validator "^4.0.0"
w3c-xmlserializer@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
dependencies:
xml-name-validator "^5.0.0"
walk-sync@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-2.2.0.tgz#80786b0657fcc8c0e1c0b1a042a09eae2966387a"
@@ -12652,11 +12753,23 @@ whatwg-encoding@^2.0.0:
dependencies:
iconv-lite "0.6.3"
whatwg-encoding@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
dependencies:
iconv-lite "0.6.3"
whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
whatwg-mimetype@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
whatwg-url@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
@@ -12665,6 +12778,14 @@ whatwg-url@^11.0.0:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6"
integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==
dependencies:
tr46 "^5.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@@ -12968,7 +13089,7 @@ write-file-atomic@^5.0.1:
imurmurhash "^0.1.4"
signal-exit "^4.0.1"
ws@8.18.0, ws@^8.11.0, ws@^8.14.2:
ws@8.18.0, ws@^8.11.0, ws@^8.14.2, ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
@@ -12978,6 +13099,11 @@ xml-name-validator@^4.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
xml-name-validator@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
@@ -13046,7 +13172,7 @@ yargs@17.7.2, yargs@^17.3.1:
y18n "^5.0.5"
yargs-parser "^21.1.1"
yjs@*, yjs@13.6.18, yjs@^13.6.15:
yjs@*, yjs@^13.6.15:
version "13.6.18"
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.18.tgz#d1575203478bc99ad1b89c098e7d4bacb7f91c3b"
integrity sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==