diff --git a/scripts/odf/fillOdtTemplate.js b/scripts/odf/fillOdtTemplate.js index a861d22..c6a7c01 100644 --- a/scripts/odf/fillOdtTemplate.js +++ b/scripts/odf/fillOdtTemplate.js @@ -1,11 +1,10 @@ import { ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayReader, TextWriter, Uint8ArrayWriter } from '@zip.js/zip.js'; import {traverse} from '../DOMUtils.js' -import makeManifestFile from './makeManifestFile.js'; +import {makeManifestFile, getManifestFileData} from './manifest.js'; -// fillOdtTemplate, getOdtTemplate, getOdtTextContent - -/** @import {ODFManifest} from './makeManifestFile.js' */ +/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */ +/** @import {ODFManifest} from './manifest.js' */ /** @typedef {ArrayBuffer} ODTFile */ @@ -329,6 +328,18 @@ function fillTemplatedOdtElement(rootElement, data, Node){ } +const keptFiles = new Set(['content.xml', 'styles.xml', 'mimetype', 'META-INF/manifest.xml']) + + +/** + * + * @param {string} filename + * @returns {boolean} + */ +function keepFile(filename){ + return keptFiles.has(filename) || filename.startsWith('Pictures/') +} + /** * @param {ODTFile} odtTemplate @@ -349,27 +360,25 @@ export default async function _fillOdtTemplate(odtTemplate, data, parseXML, seri const writer = new ZipWriter(new Uint8ArrayWriter()); /** @type {ODFManifest} */ - const manifestFileData = { - mediaType: ODTMimetype, - version: '1.3', // default, but may be changed - fileEntries: [] - } - - const keptFiles = new Set(['content.xml', 'styles.xml', 'mimetype']) + let manifestFileData; + /** @type {{filename: string, content: Reader, options?: ZipWriterAddDataOptions}[]} */ + const zipEntriesToAdd = [] // Parcourir chaque entrée du fichier ODT for await (const entry of entries) { const filename = entry.filename + //console.log('entry', filename, entry.directory) + // remove other files - if(!keptFiles.has(filename)){ + if(!keepFile(filename)){ // ignore, do not create a corresponding entry in the new zip } else{ - let content; - let options; - + let content + let options + switch(filename){ case 'mimetype': content = new TextReader(ODTMimetype) @@ -379,49 +388,65 @@ export default async function _fillOdtTemplate(odtTemplate, data, parseXML, seri dataDescriptor: false, extendedTimestamp: false, } + + zipEntriesToAdd.push({filename, content, options}) + break; case 'content.xml': + // @ts-ignore const contentXml = await entry.getData(new TextWriter()); const contentDocument = parseXML(contentXml); fillTemplatedOdtElement(contentDocument, data, Node) const updatedContentXml = serializeToString(contentDocument) - const docContentElement = contentDocument.getElementsByTagName('office:document-content')[0] - const version = docContentElement.getAttribute('office:version') - - //console.log('version', version) - manifestFileData.version = version - manifestFileData.fileEntries.push({ - fullPath: filename, - mediaType: 'text/xml' - }) - content = new TextReader(updatedContentXml) options = { lastModDate: entry.lastModDate, level: 9 }; + zipEntriesToAdd.push({filename, content, options}) + break; + + case 'META-INF/manifest.xml': + // @ts-ignore + const manifestXml = await entry.getData(new TextWriter()); + const manifestDocument = parseXML(manifestXml); + manifestFileData = getManifestFileData(manifestDocument) + + break; + case 'styles.xml': + default: const blobWriter = new BlobWriter(); + // @ts-ignore await entry.getData(blobWriter); const blob = await blobWriter.getData(); - - manifestFileData.fileEntries.push({ - fullPath: filename, - mediaType: 'text/xml' - }) content = new BlobReader(blob) + zipEntriesToAdd.push({filename, content}) break; - default: - throw new Error(`Unexpected file (${filename})`) } - - await writer.add(filename, content, options); } + } + + for(const {filename, content, options} of zipEntriesToAdd){ + await writer.add(filename, content, options); + } + + const newZipFilenames = new Set(zipEntriesToAdd.map(ze => ze.filename)) + + if(!manifestFileData){ + throw new Error(`'META-INF/manifest.xml' zip entry missing`) + } + + // remove ignored files from manifest.xml + for(const filename of manifestFileData.fileEntries.keys()){ + if(!newZipFilenames.has(filename)){ + manifestFileData.fileEntries.delete(filename) + } } const manifestFileXml = makeManifestFile(manifestFileData) diff --git a/scripts/odf/makeManifestFile.js b/scripts/odf/makeManifestFile.js deleted file mode 100644 index bac5bdc..0000000 --- a/scripts/odf/makeManifestFile.js +++ /dev/null @@ -1,44 +0,0 @@ - -/* - As specified by https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752825_826425813 -*/ - -/** @typedef {'application/vnd.oasis.opendocument.text' | 'application/vnd.oasis.opendocument.spreadsheet'} ODFMediaType */ - -/** @typedef {'1.2' | '1.3' | '1.4'} ODFVersion */ - -/** - * @typedef ODFManifestFileEntry - * @prop {string} fullPath - * @prop {string} mediaType - * @prop {string} [version] - */ - -/** - * @typedef ODFManifest - * @prop {ODFMediaType} mediaType - * @prop {ODFVersion} version - * @prop {ODFManifestFileEntry[]} fileEntries - */ - -/** - * - * @param {ODFManifestFileEntry} fileEntry - * @returns {string} - */ -function makeFileEntry({fullPath, mediaType}){ - return `` -} - -/** - * - * @param {ODFManifest} odfManifest - * @returns {string} - */ -export default function makeManifestFile({fileEntries, mediaType, version}){ - return ` - - - ${fileEntries.map(makeFileEntry).join('\n')} -` -} \ No newline at end of file diff --git a/scripts/odf/manifest.js b/scripts/odf/manifest.js new file mode 100644 index 0000000..5f8d149 --- /dev/null +++ b/scripts/odf/manifest.js @@ -0,0 +1,103 @@ + +/* + As specified by https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752825_826425813 +*/ + +/** @typedef {'application/vnd.oasis.opendocument.text' | 'application/vnd.oasis.opendocument.spreadsheet'} ODFMediaType */ + +/** @typedef {'1.2' | '1.3' | '1.4'} ODFVersion */ + +/** + * @typedef ODFManifestFileEntry + * @prop {string} fullPath + * @prop {string} mediaType + * @prop {string} [version] + */ + +/** + * @typedef ODFManifest + * @prop {ODFMediaType} mediaType + * @prop {ODFVersion} version + * @prop {Map} fileEntries + */ + +/** + * + * @param {ODFManifestFileEntry} fileEntry + * @returns {string} + */ +function makeFileEntry({fullPath, mediaType}){ + return `` +} + +/** + * + * @param {ODFManifest} odfManifest + * @returns {string} + */ +export function makeManifestFile({fileEntries, mediaType, version}){ + return ` + + + ${[...fileEntries.values()].map(makeFileEntry).join('\n')} +` +} + +/** + * @param {Document} manifestDoc + * @returns {ODFManifest} + */ +export function getManifestFileData(manifestDoc){ + /** @type {Partial>} */ + const manifestData = { + fileEntries: new Map() + } + + const manifestEl = manifestDoc.getElementsByTagName('manifest:manifest')[0] + /** @type {ODFVersion} */ + // @ts-ignore + const version = manifestEl.getAttribute('manifest:version'); + if(!version){ + throw new Error(`Missing version attibute in manifest:manifest element of manifest.xml file`) + } + + manifestData.version = version + + const manifestEntryEls = manifestEl.getElementsByTagName('manifest:file-entry') + + for(const manifestEntryEl of Array.from(manifestEntryEls)){ + /** @type {ODFManifestFileEntry} */ + const odfManifestFileEntry = { + fullPath: '', + mediaType: '' + } + + const fullPath = manifestEntryEl.getAttribute('manifest:full-path') + if(!fullPath){ + throw new Error(`Missing manifest:full-path attribute in manifest entry`) + } + odfManifestFileEntry.fullPath = fullPath + + const mediaType = manifestEntryEl.getAttribute('manifest:media-type') + if(!mediaType){ + throw new Error(`Missing manifest:media-type attribute in manifest entry for '${fullPath}'`) + } + odfManifestFileEntry.mediaType = mediaType + + if(fullPath === '/'){ + // @ts-ignore + manifestData.mediaType = mediaType + } + + const version = manifestEntryEl.getAttribute('manifest:version') + if(version){ + odfManifestFileEntry.version = version + } + + // @ts-ignore + manifestData.fileEntries.set(fullPath, odfManifestFileEntry) + } + + //@ts-ignore + return manifestData +} \ No newline at end of file diff --git a/tests/_helpers/zip-analysis.js b/tests/_helpers/zip-analysis.js new file mode 100644 index 0000000..3323076 --- /dev/null +++ b/tests/_helpers/zip-analysis.js @@ -0,0 +1,11 @@ +import { ZipReader, Uint8ArrayReader } from '@zip.js/zip.js'; + +/** + * + * @param {ArrayBuffer} odtTemplate + * @returns {ReturnType} + */ +export async function listZipEntries(odtTemplate){ + const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtTemplate))); + return reader.getEntries(); +} \ No newline at end of file diff --git a/tests/data/template-avec-image.odt b/tests/data/template-avec-image.odt new file mode 100644 index 0000000..3328146 Binary files /dev/null and b/tests/data/template-avec-image.odt differ diff --git a/tests/fill-odt-template.js b/tests/fill-odt-template.js index f2bce51..073f8cf 100644 --- a/tests/fill-odt-template.js +++ b/tests/fill-odt-template.js @@ -4,6 +4,7 @@ import {join} from 'node:path'; import {getOdtTemplate, getOdtTextContent} from '../scripts/odf/odtTemplate-forNode.js' import {fillOdtTemplate} from '../scripts/node.js' +import { listZipEntries } from './_helpers/zip-analysis.js'; test('basic template filling with variable substitution', async t => { @@ -312,3 +313,32 @@ Année +test('template filling preserves images', async t => { + const templatePath = join(import.meta.dirname, './data/template-avec-image.odt') + + const data = { + commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶` + } + + const odtTemplate = await getOdtTemplate(templatePath) + const templateEntries = await listZipEntries(odtTemplate) + + //console.log('templateEntries', templateEntries.map(({filename, directory}) => ({filename, directory}))) + + t.assert( + templateEntries.find(entry => entry.filename.startsWith('Pictures/')), + `One zip entry of the template is expected to have a name that starts with 'Pictures/'` + ) + + const odtResult = await fillOdtTemplate(odtTemplate, data) + const resultEntries = await listZipEntries(odtResult) + + //console.log('resultEntries', resultEntries.map(({filename, directory}) => ({filename, directory}))) + + + t.assert( + resultEntries.find(entry => entry.filename.startsWith('Pictures/')), + `One zip entry of the result is expected to have a name that starts with 'Pictures/'` + ) + +}) \ No newline at end of file diff --git a/tools/create-odt-file-from-template.js b/tools/create-odt-file-from-template.js index 89e474d..2bbb0f7 100644 --- a/tools/create-odt-file-from-template.js +++ b/tools/create-odt-file-from-template.js @@ -1,3 +1,4 @@ +import {writeFile} from 'node:fs/promises' import {join} from 'node:path'; import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js' @@ -38,6 +39,8 @@ const data = { ] }*/ + +/* const templatePath = join(import.meta.dirname, '../tests/data/tableau-simple.odt') const data = { annéeConsos : [ @@ -49,9 +52,15 @@ const data = { { année: 2020, conso: 37859.246}, ] } +*/ +const templatePath = join(import.meta.dirname, '../tests/data/template-avec-image.odt') + +const data = { + commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶` +} const odtTemplate = await getOdtTemplate(templatePath) const odtResult = await fillOdtTemplate(odtTemplate, data) -process.stdout.write(new Uint8Array(odtResult)) \ No newline at end of file +writeFile('yo.odt', new Uint8Array(odtResult))