From 57dfb4f050e05b74edcd9960df540df7e0ce07e7 Mon Sep 17 00:00:00 2001 From: David Bruant Date: Mon, 7 Apr 2025 11:00:18 +0200 Subject: [PATCH] Templ odt (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add rough roadmap * adding test of odt template filling * progress * avec les titres * premier jet de remplacement * progress i guess * Bimz ! Remplissage de template et passage de tests ! * nettoyages * zip entries async generator * un fichier d'exemple pour la génération d'une liste de courses * Test avec le each et amélioration de getOdtTextContent * yep * Le test du each passe mais pas encore la création du fichier .odt * Meilleur packaging du zip * Création d'un fichier à partir d'un template - le fichier de sortie s'ouvre avec LibreOffice !! * Génération d'une liste dans un .odt * 2 sibling each in a document * add nested each test * Génération d'un tableau avec un {#each} * Refacto API for Node.js * add fillOdtTemplate to browser exports * Mention template filling in readme --- package-lock.json | 17 +- package.json | 2 +- readme.md | 70 +++- scripts/DOMUtils.js | 23 ++ scripts/browser.js | 14 + scripts/createOdsFile.js | 3 + scripts/node.js | 20 +- scripts/odf/fillOdtTemplate.js | 439 ++++++++++++++++++++++++ scripts/odf/makeManifestFile.js | 44 +++ scripts/odf/odtTemplate-forNode.js | 87 +++++ tests/data/enum-courses.odt | Bin 0 -> 13607 bytes tests/data/liste-courses.odt | Bin 0 -> 14067 bytes tests/data/liste-fruits-et-légumes.odt | Bin 0 -> 13867 bytes tests/data/légumes-de-saison.odt | Bin 0 -> 14869 bytes tests/data/tableau-simple.odt | Bin 0 -> 15861 bytes tests/data/template-anniversaire.odt | Bin 0 -> 11174 bytes tests/fill-odt-template.js | 314 +++++++++++++++++ tools/create-odt-file-from-template.js | 57 +++ 18 files changed, 1071 insertions(+), 19 deletions(-) create mode 100644 scripts/DOMUtils.js create mode 100644 scripts/odf/fillOdtTemplate.js create mode 100644 scripts/odf/makeManifestFile.js create mode 100644 scripts/odf/odtTemplate-forNode.js create mode 100644 tests/data/enum-courses.odt create mode 100644 tests/data/liste-courses.odt create mode 100644 tests/data/liste-fruits-et-légumes.odt create mode 100644 tests/data/légumes-de-saison.odt create mode 100644 tests/data/tableau-simple.odt create mode 100644 tests/data/template-anniversaire.odt create mode 100644 tests/fill-odt-template.js create mode 100644 tools/create-odt-file-from-template.js diff --git a/package-lock.json b/package-lock.json index 0982f14..495bd81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "ods-xlsx", "version": "0.11.0", "dependencies": { - "@xmldom/xmldom": "^0.8.10", + "@xmldom/xmldom": "^0.9.8", "@zip.js/zip.js": "^2.7.57" }, "devDependencies": { @@ -604,11 +604,12 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", + "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/@zip.js/zip.js": { @@ -4921,9 +4922,9 @@ } }, "@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==" + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==" }, "@zip.js/zip.js": { "version": "2.7.57", diff --git a/package.json b/package.json index f2de6aa..c52e9ae 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "svelte-preprocess": "^5.1.3" }, "dependencies": { - "@xmldom/xmldom": "^0.8.10", + "@xmldom/xmldom": "^0.9.8", "@zip.js/zip.js": "^2.7.57" } } diff --git a/readme.md b/readme.md index 3db1cb2..2b3b29e 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,16 @@ Small lib to parse/understand .ods and .xsls files in the browser and node.js +## Rough roadmap + +- [ ] add odt templating +- [ ] remove support for xlsx +- [ ] add a .ods minifyer +- [ ] add a generic .ods visualizer +- [ ] move to a dedicated odf docs org +- [ ] add a quick .odt visualiser (maybe converting to markdown first?) + + ## Usage ### Install @@ -12,19 +22,17 @@ npm i https://github.com/DavidBruant/ods-xlsx.git#v0.11.0 ``` -### Usage - -#### Basic - reading an ods/xlsx file +### Basic - reading an ods/xlsx file ```js import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent} from 'ods-xlsx' /** - * @param {File} file - an .ods file like the ones you get from an + * @param {ArrayBuffer} odsFile - content of an .ods file * @return {Promise} */ -async function getFileData(file){ - return tableRawContent +async function getFileData(odsFile){ + return getODSTableRawContent(odsFile) .then(tableWithoutEmptyRows) .then(tableRawContentToObjects) } @@ -36,7 +44,7 @@ the **values** are automatically converted from the .ods or .xlsx files (which t to the appropriate JavaScript value -#### Basic - creating an ods file +### Basic - creating an ods file ```js import {createOdsFile} from 'ods-xlsx' @@ -72,9 +80,51 @@ const ods = await createOdsFile(content) (and there is a tool to test file creation: `node tools/create-an-ods-file.js > yo.ods`) -#### Low-level -See exports +### Filling an .odt template + +odf.js proposes a template syntax + +In an .odt file, write the following: + +```txt +Hey {nom}! + +Your birthdate is {dateNaissance} +``` + +And then run the code: + + +```js +import {join} from 'node:path'; + +import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js' +import {fillOdtTemplate} from '../scripts/node.js' + +// replace with your template path +const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt') +const data = { + nom: 'David Bruant', + dateNaissance: '8 mars 1987' +} + +const odtTemplate = await getOdtTemplate(templatePath) +const odtResult = await fillOdtTemplate(odtTemplate, data) + +process.stdout.write(new Uint8Array(odtResult)) +``` + +There are also loops in the form: + +```txt +- {#each listeCourses as élément} +- {élément} +- {/each} +``` + +They can be used to generate lists or tables in .odt files from data and a template using this syntax + ### Demo @@ -89,6 +139,8 @@ npm run dev ``` + + ## Expectations and licence I hope to be credited for the work on this repo diff --git a/scripts/DOMUtils.js b/scripts/DOMUtils.js new file mode 100644 index 0000000..ff0b1d6 --- /dev/null +++ b/scripts/DOMUtils.js @@ -0,0 +1,23 @@ +/* + Since we're using xmldom in Node.js context, the entire DOM API is not implemented + Functions here are helpers whild xmldom becomes more complete +*/ + +/** + * Traverses a DOM tree starting from the given element and applies the visit function + * to each Element node encountered in tree order (depth-first). + * + * This should probably be replace by the TreeWalker API when implemented by xmldom + * + * @param {Node} node + * @param {(n : Node) => void} visit + */ +export function traverse(node, visit) { + //console.log('traverse', node.nodeType, node.nodeName) + + for (const child of Array.from(node.childNodes)) { + traverse(child, visit); + } + + visit(node); +} diff --git a/scripts/browser.js b/scripts/browser.js index b2e5a77..dc6e03e 100644 --- a/scripts/browser.js +++ b/scripts/browser.js @@ -7,7 +7,11 @@ import { import {_createOdsFile} from './createOdsFile.js' +import _fillOdtTemplate from './odf/fillOdtTemplate.js' + + /** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */ +/** @import {ODTFile} from './odf/fillOdtTemplate.js' */ function parseXML(str){ @@ -44,6 +48,16 @@ const serializeToString = function serializeToString(node){ return serializer.serializeToString(node) } +/** + * @param {ODTFile} odtTemplate + * @param {any} data + * @returns {Promise} + */ +export function fillOdtTemplate(odtTemplate, data){ + return _fillOdtTemplate(odtTemplate, data, parseXML, serializeToString, Node) +} + + /** * @param {Map} sheetsData */ diff --git a/scripts/createOdsFile.js b/scripts/createOdsFile.js index 5d8a96c..cbd9170 100644 --- a/scripts/createOdsFile.js +++ b/scripts/createOdsFile.js @@ -30,6 +30,9 @@ export async function _createOdsFile(sheetsData, createDocument, serializeToStri // Create a new zip writer const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet')); + // The “mimetype” file shall be the first file of the zip file. + // It shall not be compressed, and it shall not use an 'extra field' in its header. + // https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813 zipWriter.add( "mimetype", new TextReader("application/vnd.oasis.opendocument.spreadsheet"), diff --git a/scripts/node.js b/scripts/node.js index 5096c40..ea0dce6 100644 --- a/scripts/node.js +++ b/scripts/node.js @@ -1,6 +1,6 @@ //@ts-check -import {DOMParser, DOMImplementation, XMLSerializer} from '@xmldom/xmldom' +import {DOMParser, DOMImplementation, XMLSerializer, Node} from '@xmldom/xmldom' import { _getODSTableRawContent, @@ -8,9 +8,17 @@ import { } from './shared.js' import { _createOdsFile } from './createOdsFile.js' +import _fillOdtTemplate from './odf/fillOdtTemplate.js' + /** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */ +/** @import {ODTFile} from './odf/fillOdtTemplate.js' */ +/** + * + * @param {string} str + * @returns {Document} + */ function parseXML(str){ return (new DOMParser()).parseFromString(str, 'application/xml'); } @@ -47,6 +55,16 @@ const serializeToString = function serializeToString(node){ return serializer.serializeToString(node) } +/** + * @param {ODTFile} odtTemplate + * @param {any} data + * @returns {Promise} + */ +export function fillOdtTemplate(odtTemplate, data){ + return _fillOdtTemplate(odtTemplate, data, parseXML, serializeToString, Node) +} + + /** * @param {Map} sheetsData */ diff --git a/scripts/odf/fillOdtTemplate.js b/scripts/odf/fillOdtTemplate.js new file mode 100644 index 0000000..a861d22 --- /dev/null +++ b/scripts/odf/fillOdtTemplate.js @@ -0,0 +1,439 @@ +import { ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayReader, TextWriter, Uint8ArrayWriter } from '@zip.js/zip.js'; + +import {traverse} from '../DOMUtils.js' +import makeManifestFile from './makeManifestFile.js'; + +// fillOdtTemplate, getOdtTemplate, getOdtTextContent + +/** @import {ODFManifest} from './makeManifestFile.js' */ + +/** @typedef {ArrayBuffer} ODTFile */ + +const ODTMimetype = 'application/vnd.oasis.opendocument.text' + + + + +// For a given string, split it into fixed parts and parts to replace + +/** + * @typedef TextPlaceToFill + * @property { {expression: string, replacedString:string}[] } expressions + * @property {(values: any) => void} fill + */ + + +/** + * PPP : for now, expression is expected to be only an object property name or a dot-path + * in the future, it will certainly be a JavaScript expression + * securely evaluated within an hardernedJS Compartment https://hardenedjs.org/#compartment + * @param {string} expression + * @param {any} context - data / global object + * @return {any} + */ +function evaludateTemplateExpression(expression, context){ + const parts = expression.trim().split('.') + + let value = context; + + for(const part of parts){ + if(!value){ + return undefined + } + else{ + value = value[part] + } + } + + return value +} + + +/** + * @param {string} str + * @returns {TextPlaceToFill | undefined} + */ +function findPlacesToFillInString(str) { + const matches = str.matchAll(/\{([^{#\/]+?)\}/g) + + /** @type {TextPlaceToFill['expressions']} */ + const expressions = [] + + /** @type {(string | ((data:any) => void))[]} */ + const parts = [] + let remaining = str; + + for (const match of matches) { + //console.log('match', match) + const [matched, group1] = match + + const replacedString = matched + const expression = group1.trim() + expressions.push({ expression, replacedString }) + + const [fixedPart, newRemaining] = remaining.split(replacedString, 2) + + if (fixedPart.length >= 1) + parts.push(fixedPart) + + + parts.push(data => evaludateTemplateExpression(expression, data)) + + remaining = newRemaining + } + + if (remaining.length >= 1) + parts.push(remaining) + + //console.log('parts', parts) + + + if (remaining === str) { + // no match found + return undefined + } + else { + return { + expressions, + fill: (data) => { + return parts.map(p => { + if (typeof p === 'string') + return p + else + return p(data) + }) + .join('') + } + } + } + + +} + + + +/** + * + * @param {Node} startNode + * @param {string} iterableExpression + * @param {string} itemExpression + * @param {Node} endNode + * @param {any} data + * @param {typeof Node} Node + */ +function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, data, Node){ + //console.log('fillEachBlock', iterableExpression, itemExpression) + //console.log('startNode', startNode.nodeType, startNode.nodeName) + //console.log('endNode', endNode.nodeType, endNode.nodeName) + + // find common ancestor + let commonAncestor + + let startAncestor = startNode + let endAncestor = endNode + + const startAncestry = new Set([startAncestor]) + const endAncestry = new Set([endAncestor]) + + while(!startAncestry.has(endAncestor) && !endAncestry.has(startAncestor)){ + if(startAncestor.parentNode){ + startAncestor = startAncestor.parentNode + startAncestry.add(startAncestor) + } + if(endAncestor.parentNode){ + endAncestor = endAncestor.parentNode + endAncestry.add(endAncestor) + } + } + + if(startAncestry.has(endAncestor)){ + commonAncestor = endAncestor + } + else{ + commonAncestor = startAncestor + } + + + //console.log('commonAncestor', commonAncestor.tagName) + //console.log('startAncestry', startAncestry.size, [...startAncestry].indexOf(commonAncestor)) + //console.log('endAncestry', endAncestry.size, [...endAncestry].indexOf(commonAncestor)) + + const startAncestryToCommonAncestor = [...startAncestry].slice(0, [...startAncestry].indexOf(commonAncestor)) + const endAncestryToCommonAncestor = [...endAncestry].slice(0, [...endAncestry].indexOf(commonAncestor)) + + const startChild = startAncestryToCommonAncestor.at(-1) + const endChild = endAncestryToCommonAncestor.at(-1) + + //console.log('startChild', startChild.tagName) + //console.log('endChild', endChild.tagName) + + // Find repeatable pattern and extract it in a documentFragment + // @ts-ignore + const repeatedFragment = startNode.ownerDocument.createDocumentFragment() + + /** @type {Element[]} */ + const repeatedPatternArray = [] + let sibling = startChild.nextSibling + + while(sibling !== endChild){ + repeatedPatternArray.push(sibling) + sibling = sibling.nextSibling; + } + + + //console.log('repeatedPatternArray', repeatedPatternArray.length) + + for(const sibling of repeatedPatternArray){ + sibling.parentNode?.removeChild(sibling) + repeatedFragment.appendChild(sibling) + } + + // Find the iterable in the data + // PPP eventually, evaluate the expression as a JS expression + const iterable = evaludateTemplateExpression(iterableExpression, data) + if(!iterable){ + throw new TypeError(`Missing iterable (${iterableExpression})`) + } + if(typeof iterable[Symbol.iterator] !== 'function'){ + throw new TypeError(`'${iterableExpression}' is not iterable`) + } + + // create each loop result + // using a for-of loop to accept all iterable values + for(const item of iterable){ + /** @type {DocumentFragment} */ + // @ts-ignore + const itemFragment = repeatedFragment.cloneNode(true) + + // recursive call to fillTemplatedOdtElement on itemFragment + fillTemplatedOdtElement( + itemFragment, + Object.assign({}, data, {[itemExpression]: item}), + Node + ) + // @ts-ignore + commonAncestor.insertBefore(itemFragment, endChild) + } + + startChild.parentNode.removeChild(startChild) + endChild.parentNode.removeChild(endChild) +} + + +/** + * + * @param {Element | DocumentFragment} rootElement + * @param {any} data + * @param {typeof Node} Node + * @returns {void} + */ +function fillTemplatedOdtElement(rootElement, data, Node){ + //console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName) + + /** @type {Node | undefined} */ + let eachBlockStartNode + /** @type {Node | undefined} */ + let eachBlockEndNode + + let nestedEach = 0 + + let iterableExpression, itemExpression; + + // Traverse "in document order" + + // @ts-ignore + traverse(rootElement, currentNode => { + const insideAnEachBlock = !!eachBlockStartNode + + if(currentNode.nodeType === Node.TEXT_NODE){ + const text = currentNode.textContent || '' + + // looking for {#each x as y} + const eachStartRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/g; + const startMatches = [...text.matchAll(eachStartRegex)]; + + if(startMatches && startMatches.length >= 1){ + if(insideAnEachBlock){ + nestedEach = nestedEach + 1 + } + else{ + // PPP for now, consider only the first set of matches + // eventually, consider all of them for in-text-node {#each}...{/each} + let [_, _iterableExpression, _itemExpression] = startMatches[0] + + iterableExpression = _iterableExpression + itemExpression = _itemExpression + eachBlockStartNode = currentNode + } + } + + // trying to find an {/each} + const eachEndRegex = /{\/each}/g + const endMatches = [...text.matchAll(eachEndRegex)]; + + if(endMatches && endMatches.length >= 1){ + if(!eachBlockStartNode) + throw new TypeError(`{/each} found without corresponding opening {#each x as y}`) + + if(nestedEach >= 1){ + // ignore because it will be treated as part of the outer {#each} + nestedEach = nestedEach - 1 + } + else{ + eachBlockEndNode = currentNode + + // found an #each and its corresponding /each + // execute replacement loop + fillEachBlock(eachBlockStartNode, iterableExpression, itemExpression, eachBlockEndNode, data, Node) + + eachBlockStartNode = undefined + iterableExpression = undefined + itemExpression = undefined + eachBlockEndNode = undefined + } + } + + + // Looking for variables for substitutions + if(!insideAnEachBlock){ + if (currentNode.data) { + const placesToFill = findPlacesToFillInString(currentNode.data) + + if(placesToFill){ + const newText = placesToFill.fill(data) + const newTextNode = currentNode.ownerDocument?.createTextNode(newText) + currentNode.parentNode?.replaceChild(newTextNode, currentNode) + } + } + } + else{ + // ignore because it will be treated as part of the {#each} block + } + } + + if(currentNode.nodeType === Node.ATTRIBUTE_NODE){ + // Looking for variables for substitutions + if(!insideAnEachBlock){ + if (currentNode.value) { + const placesToFill = findPlacesToFillInString(currentNode.value) + if(placesToFill){ + currentNode.value = placesToFill.fill(data) + } + } + } + else{ + // ignore because it will be treated as part of the {#each} block + } + } + }) +} + + + +/** + * @param {ODTFile} odtTemplate + * @param {any} data + * @param {Function} parseXML + * @param {typeof XMLSerializer.prototype.serializeToString} serializeToString + * @param {typeof Node} Node + * @returns {Promise} + */ +export default async function _fillOdtTemplate(odtTemplate, data, parseXML, serializeToString, Node) { + + const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtTemplate))); + + // Lire toutes les entrées du fichier ODT + const entries = reader.getEntriesGenerator(); + + // Créer un ZipWriter pour le nouveau fichier ODT + 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']) + + + // Parcourir chaque entrée du fichier ODT + for await (const entry of entries) { + const filename = entry.filename + + // remove other files + if(!keptFiles.has(filename)){ + // ignore, do not create a corresponding entry in the new zip + } + else{ + let content; + let options; + + switch(filename){ + case 'mimetype': + content = new TextReader(ODTMimetype) + options = { + level: 0, + compressionMethod: 0, + dataDescriptor: false, + extendedTimestamp: false, + } + break; + case 'content.xml': + 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 + }; + + break; + case 'styles.xml': + const blobWriter = new BlobWriter(); + await entry.getData(blobWriter); + const blob = await blobWriter.getData(); + + manifestFileData.fileEntries.push({ + fullPath: filename, + mediaType: 'text/xml' + }) + + content = new BlobReader(blob) + break; + default: + throw new Error(`Unexpected file (${filename})`) + } + + await writer.add(filename, content, options); + } + + } + + const manifestFileXml = makeManifestFile(manifestFileData) + await writer.add('META-INF/manifest.xml', new TextReader(manifestFileXml)); + + await reader.close(); + + return writer.close(); +} + + + + + + diff --git a/scripts/odf/makeManifestFile.js b/scripts/odf/makeManifestFile.js new file mode 100644 index 0000000..bac5bdc --- /dev/null +++ b/scripts/odf/makeManifestFile.js @@ -0,0 +1,44 @@ + +/* + 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/odtTemplate-forNode.js b/scripts/odf/odtTemplate-forNode.js new file mode 100644 index 0000000..e1f19f2 --- /dev/null +++ b/scripts/odf/odtTemplate-forNode.js @@ -0,0 +1,87 @@ +import { readFile } from 'node:fs/promises' + +import { ZipReader, Uint8ArrayReader, TextWriter } from '@zip.js/zip.js'; +import {DOMParser, Node} from '@xmldom/xmldom' + + +/** @import {ODTFile} from './fillOdtTemplate.js' */ + + +/** + * + * @param {Document} odtDocument + * @returns {Element} + */ +function getODTTextElement(odtDocument) { + return odtDocument.getElementsByTagName('office:body')[0] + .getElementsByTagName('office:text')[0] +} + + +/** + * + * @param {string} path + * @returns {Promise} + */ +export async function getOdtTemplate(path) { + const fileBuffer = await readFile(path) + return fileBuffer.buffer +} + +/** + * Extracts plain text content from an ODT file, preserving line breaks + * @param {ArrayBuffer} odtFile - The ODT file as an ArrayBuffer + * @returns {Promise} Extracted text content + */ +export async function getOdtTextContent(odtFile) { + const contentDocument = await getContentDocument(odtFile) + const odtTextElement = getODTTextElement(contentDocument) + + /** + * + * @param {Element} element + * @returns {string} + */ + function getElementTextContent(element){ + //console.log('tagName', element.tagName) + if(element.tagName === 'text:h' || element.tagName === 'text:p') + return element.textContent + '\n' + else{ + const descendantTexts = Array.from(element.childNodes) + .filter(n => n.nodeType === Node.ELEMENT_NODE) + .map(getElementTextContent) + + if(element.tagName === 'text:list-item') + return `- ${descendantTexts.join('')}` + + return descendantTexts.join('') + } + } + + return getElementTextContent(odtTextElement) +} + + +/** + * @param {ODTFile} odtFile + * @returns {Promise} + */ +async function getContentDocument(odtFile) { + const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtFile))); + + const entries = await reader.getEntries(); + + const contentEntry = entries.find(entry => entry.filename === 'content.xml'); + + if (!contentEntry) { + throw new Error('No content.xml found in the ODT file'); + } + + // @ts-ignore + const contentText = await contentEntry.getData(new TextWriter()); + await reader.close(); + + const parser = new DOMParser(); + + return parser.parseFromString(contentText, 'text/xml'); +} \ No newline at end of file diff --git a/tests/data/enum-courses.odt b/tests/data/enum-courses.odt new file mode 100644 index 0000000000000000000000000000000000000000..f218dff1ca33d9b81a365610db2b3dc3aa9b01a1 GIT binary patch literal 13607 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 literal 0 HcmV?d00001 diff --git a/tests/data/liste-courses.odt b/tests/data/liste-courses.odt new file mode 100644 index 0000000000000000000000000000000000000000..a3365c3aa22726e1b8f02bdc603ec18064a1084f GIT binary patch literal 14067 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 I<4f)T0L`^Ly8r+H literal 0 HcmV?d00001 diff --git a/tests/data/liste-fruits-et-légumes.odt b/tests/data/liste-fruits-et-légumes.odt new file mode 100644 index 0000000000000000000000000000000000000000..ac5bdebd2562c815081a11c0caace5f5c040a664 GIT binary patch literal 13867 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 literal 0 HcmV?d00001 diff --git a/tests/data/légumes-de-saison.odt b/tests/data/légumes-de-saison.odt new file mode 100644 index 0000000000000000000000000000000000000000..d56d2ccfbc8724a94f67936480851d8f5f0847ca GIT binary patch literal 14869 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 Q{I7q8ubWO1)6cvA1>J9cpa1{> literal 0 HcmV?d00001 diff --git a/tests/data/tableau-simple.odt b/tests/data/tableau-simple.odt new file mode 100644 index 0000000000000000000000000000000000000000..e7d5d767cd8f95ee7996f0c6071e4691ec221373 GIT binary patch literal 15861 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 He;xfVwk&fw literal 0 HcmV?d00001 diff --git a/tests/data/template-anniversaire.odt b/tests/data/template-anniversaire.odt new file mode 100644 index 0000000000000000000000000000000000000000..3f0b824cab7fe41344af8b0ea412e3d8e2708e36 GIT binary patch literal 11174 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 literal 0 HcmV?d00001 diff --git a/tests/fill-odt-template.js b/tests/fill-odt-template.js new file mode 100644 index 0000000..f2bce51 --- /dev/null +++ b/tests/fill-odt-template.js @@ -0,0 +1,314 @@ +import test from 'ava'; +import {join} from 'node:path'; + +import {getOdtTemplate, getOdtTextContent} from '../scripts/odf/odtTemplate-forNode.js' + +import {fillOdtTemplate} from '../scripts/node.js' + + +test('basic template filling with variable substitution', async t => { + const templatePath = join(import.meta.dirname, './data/template-anniversaire.odt') + const templateContent = `Yo {nom} ! +Tu es né.e le {dateNaissance} + +Bonjoir ☀️ +` + + const data = { + nom: 'David Bruant', + dateNaissance: '8 mars 1987' + } + + const odtTemplate = await getOdtTemplate(templatePath) + const templateTextContent = await getOdtTextContent(odtTemplate) + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent, `Yo David Bruant ! +Tu es né.e le 8 mars 1987 + +Bonjoir ☀️ +`) + +}); + + + +test('basic template filling with {#each}', async t => { + const templatePath = join(import.meta.dirname, './data/enum-courses.odt') + const templateContent = `🧺 La liste de courses incroyable 🧺 + +{#each listeCourses as élément} +{élément} +{/each} +` + + const data = { + listeCourses : [ + 'Radis', + `Jus d'orange`, + 'Pâtes à lasagne (fraîches !)' + ] + } + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent, `🧺 La liste de courses incroyable 🧺 + +Radis +Jus d'orange +Pâtes à lasagne (fraîches !) +`) + + +}); + + + +test('template filling with {#each} generating a list', async t => { + const templatePath = join(import.meta.dirname, './data/liste-courses.odt') + const templateContent = `🧺 La liste de courses incroyable 🧺 + +- {#each listeCourses as élément} +- {élément} +- {/each} +` + + const data = { + listeCourses : [ + 'Radis', + `Jus d'orange`, + 'Pâtes à lasagne (fraîches !)' + ] + } + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent, `🧺 La liste de courses incroyable 🧺 + +- Radis +- Jus d'orange +- Pâtes à lasagne (fraîches !) +`) + + +}); + + +test('template filling with 2 sequential {#each}', async t => { + const templatePath = join(import.meta.dirname, './data/liste-fruits-et-légumes.odt') + const templateContent = `Liste de fruits et légumes + +Fruits +{#each fruits as fruit} +{fruit} +{/each} + +Légumes +{#each légumes as légume} +{légume} +{/each} +` + + const data = { + fruits : [ + 'Pastèque 🍉', + `Kiwi 🥝`, + 'Banane 🍌' + ], + légumes: [ + 'Champignon 🍄‍🟫', + 'Avocat 🥑', + 'Poivron 🫑' + ] + } + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent, `Liste de fruits et légumes + +Fruits +Pastèque 🍉 +Kiwi 🥝 +Banane 🍌 + +Légumes +Champignon 🍄‍🟫 +Avocat 🥑 +Poivron 🫑 +`) + +}); + + + +test('template filling with nested {#each}s', async t => { + const templatePath = join(import.meta.dirname, './data/légumes-de-saison.odt') + const templateContent = `Légumes de saison + +{#each légumesSaison as saisonLégumes} +{saisonLégumes.saison} +- {#each saisonLégumes.légumes as légume} +- {légume} +- {/each} + +{/each} +` + + const data = { + légumesSaison : [ + { + saison: 'Printemps', + légumes: [ + 'Asperge', + 'Betterave', + 'Blette' + ] + }, + { + saison: 'Été', + légumes: [ + 'Courgette', + 'Poivron', + 'Laitue' + ] + }, + { + saison: 'Automne', + légumes: [ + 'Poireau', + 'Potiron', + 'Brocoli' + ] + }, + { + saison: 'Hiver', + légumes: [ + 'Radis', + 'Chou de Bruxelles', + 'Frisée' + ] + } + ] + } + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent, `Légumes de saison + +Printemps +- Asperge +- Betterave +- Blette + +Été +- Courgette +- Poivron +- Laitue + +Automne +- Poireau +- Potiron +- Brocoli + +Hiver +- Radis +- Chou de Bruxelles +- Frisée + +`) + +}); + + + +test('template filling of a table', async t => { + const templatePath = join(import.meta.dirname, './data/tableau-simple.odt') + const templateContent = `Évolution énergie en kWh par personne en France + +Année +Énergie par personne +{#each annéeConsos as annéeConso} + +{annéeConso.année} +{annéeConso.conso} +{/each} +` + + /* + Data sources: + + U.S. Energy Information Administration (2023)Energy Institute - + Statistical Review of World Energy (2024)Population based on various sources (2023) + + – with major processing by Our World in Data + */ + const data = { + annéeConsos : [ + { année: 1970, conso: 36252.637}, + { année: 1980, conso: 43328.78}, + { année: 1990, conso: 46971.94}, + { année: 2000, conso: 53147.277}, + { année: 2010, conso: 48062.32}, + { année: 2020, conso: 37859.246}, + ] + } + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent.trim(), `Évolution énergie en kWh par personne en France + +Année +Énergie par personne +1970 +36252.637 +1980 +43328.78 +1990 +46971.94 +2000 +53147.277 +2010 +48062.32 +2020 +37859.246 +`.trim()) + +}); + + + diff --git a/tools/create-odt-file-from-template.js b/tools/create-odt-file-from-template.js new file mode 100644 index 0000000..89e474d --- /dev/null +++ b/tools/create-odt-file-from-template.js @@ -0,0 +1,57 @@ +import {join} from 'node:path'; + +import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js' +import {fillOdtTemplate} from '../scripts/node.js' + +/* +const templatePath = join(import.meta.dirname, '../tests/data/template-anniversaire.odt') +const data = { + nom: 'David Bruant', + dateNaissance: '8 mars 1987' +} +*/ + + +/* +const templatePath = join(import.meta.dirname, '../tests/data/liste-courses.odt') +const data = { + listeCourses : [ + 'Radis', + `Jus d'orange`, + 'Pâtes à lasagne (fraîches !)' + ] +} +*/ + +/* +const templatePath = join(import.meta.dirname, '../tests/data/liste-fruits-et-légumes.odt') +const data = { + fruits : [ + 'Pastèque 🍉', + `Kiwi 🥝`, + 'Banane 🍌' + ], + légumes: [ + 'Champignon 🍄‍🟫', + 'Avocat 🥑', + 'Poivron 🫑' + ] +}*/ + +const templatePath = join(import.meta.dirname, '../tests/data/tableau-simple.odt') +const data = { + annéeConsos : [ + { année: 1970, conso: 36252.637}, + { année: 1980, conso: 43328.78}, + { année: 1990, conso: 46971.94}, + { année: 2000, conso: 53147.277}, + { année: 2010, conso: 48062.32}, + { année: 2020, conso: 37859.246}, + ] +} + + +const odtTemplate = await getOdtTemplate(templatePath) +const odtResult = await fillOdtTemplate(odtTemplate, data) + +process.stdout.write(new Uint8Array(odtResult)) \ No newline at end of file