Skip to content

Commit 8d3543b

Browse files
committed
✨(docs) add title metadata to exported docx/pdf for accessibility
ensures document title is preserved in exports to meet accessibility needs Signed-off-by: Cyril <c.gromoff@gmail.com>
1 parent 62e122b commit 8d3543b

File tree

4 files changed

+59
-6
lines changed

4 files changed

+59
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to
1616
- #1271
1717
- #1341
1818
- #1362
19+
- #1379
1920

2021
### Changed
2122

src/frontend/apps/impress/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"i18next": "25.5.2",
4949
"i18next-browser-languagedetector": "8.2.0",
5050
"idb": "8.0.3",
51+
"jszip": "3.10.1",
5152
"lodash": "4.17.21",
5253
"luxon": "3.7.2",
5354
"next": "15.5.3",

src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl';
2424
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
2525
import { docxDocsSchemaMappings } from '../mappingDocx';
2626
import { pdfDocsSchemaMappings } from '../mappingPDF';
27-
import { downloadFile } from '../utils';
27+
import { addTitleToDocx, downloadFile } from '../utils';
2828

2929
enum DocDownloadFormat {
3030
PDF = 'pdf',
@@ -76,12 +76,14 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
7676

7777
setIsExporting(true);
7878

79-
const title = (doc.title || untitledDocument)
79+
const filename = (doc.title || untitledDocument)
8080
.toLowerCase()
8181
.normalize('NFD')
8282
.replace(/[\u0300-\u036f]/g, '')
8383
.replace(/\s/g, '-');
8484

85+
const documentTitle = doc.title || untitledDocument;
86+
8587
const html = templateSelected;
8688
let exportDocument = editor.document;
8789
if (html) {
@@ -98,9 +100,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
98100
exportDocument,
99101
)) as React.ReactElement<DocumentProps>;
100102

101-
// Inject language for screen reader support
103+
// Inject language and title for screen reader support and accessibility
102104
const pdfDocument = isValidElement(rawPdfDocument)
103-
? cloneElement(rawPdfDocument, { language: i18next.language })
105+
? cloneElement(rawPdfDocument, {
106+
language: i18next.language,
107+
title: documentTitle,
108+
})
104109
: rawPdfDocument;
105110

106111
blobExport = await pdf(pdfDocument).toBlob();
@@ -109,10 +114,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
109114
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
110115
});
111116

112-
blobExport = await exporter.toBlob(exportDocument);
117+
const originalBlob = await exporter.toBlob(exportDocument);
118+
119+
blobExport = await addTitleToDocx(originalBlob, documentTitle);
113120
}
114121

115-
downloadFile(blobExport, `${title}.${format}`);
122+
downloadFile(blobExport, `${filename}.${format}`);
116123

117124
toast(
118125
t('Your {{format}} was downloaded succesfully', {

src/frontend/apps/impress/src/features/docs/doc-export/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from '@blocknote/core';
66
import { Canvg } from 'canvg';
77
import { IParagraphOptions, ShadingType } from 'docx';
8+
import JSZip from 'jszip';
89

910
export function downloadFile(blob: Blob, filename: string) {
1011
const url = window.URL.createObjectURL(blob);
@@ -101,3 +102,46 @@ export function docxBlockPropsToStyles(
101102
})(),
102103
};
103104
}
105+
106+
/**
107+
* Adds title metadata to a DOCX blob by modifying the core.xml file
108+
* @param docxBlob - The original DOCX blob
109+
* @param title - The title to set in the document metadata
110+
* @returns Promise<Blob> - A new DOCX blob with the title metadata added
111+
*/
112+
export async function addTitleToDocx(
113+
docxBlob: Blob,
114+
title: string,
115+
): Promise<Blob> {
116+
try {
117+
const zip = new JSZip();
118+
await zip.loadAsync(docxBlob);
119+
120+
const coreXmlFile = zip.file('docProps/core.xml');
121+
122+
if (coreXmlFile) {
123+
let coreXmlContent = await coreXmlFile.async('text');
124+
125+
// Check if there's already a title element and replace it, or add a new one
126+
if (coreXmlContent.includes('<dc:title>')) {
127+
coreXmlContent = coreXmlContent.replace(
128+
/<dc:title>.*?<\/dc:title>/,
129+
`<dc:title>${title}</dc:title>`,
130+
);
131+
} else {
132+
// Add title element before the closing </cp:coreProperties> tag
133+
coreXmlContent = coreXmlContent.replace(
134+
/<\/cp:coreProperties>/,
135+
`<dc:title>${title}</dc:title></cp:coreProperties>`,
136+
);
137+
}
138+
zip.file('docProps/core.xml', coreXmlContent);
139+
}
140+
141+
return await zip.generateAsync({ type: 'blob' });
142+
} catch (error) {
143+
console.warn('Failed to add title to DOCX metadata:', error);
144+
// Return original blob if modification fails
145+
return docxBlob;
146+
}
147+
}

0 commit comments

Comments
 (0)