Compare commits

...

28 Commits

Author SHA1 Message Date
e6e09a0361 Properly support currency fields
Some checks failed
Build and Deploy / build (push) Failing after 5m8s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 03:32:14 -07:00
3d98e41d2b Add column width calculation
Some checks failed
Build and Deploy / build (push) Failing after 11s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 01:54:56 -07:00
7c87d3220d Update repository url
Some checks failed
Build and Deploy / build (push) Failing after 12s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 01:10:55 -07:00
8b60ceb39c Add bold cell formatting option
Some checks failed
Build and Deploy / build (push) Failing after 14s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 01:10:10 -07:00
Hannaeko
c677c7267b bump readme version 2025-09-22 16:35:43 +02:00
Hannaeko
415a26e3f1 0.30.0 2025-09-22 16:35:13 +02:00
hannaeko
90b97e23e9
fix text extraction for cells with partial styling (#23) 2025-09-22 16:34:43 +02:00
David Bruant
02d5338634 bump readme version 2025-09-18 16:16:21 +02:00
David Bruant
542183a593 0.29.0 2025-09-18 16:15:55 +02:00
David Bruant
a31f57026d
Remove default call to lockdown (#22)
* restore test

* suppression de l'appel à lockdown par défaut

* improving readme

* ses@1.14

* add section on securing fillOdtTemplate
2025-09-18 16:15:24 +02:00
David Bruant
98817e9d34 bump readme version 2025-09-18 10:41:35 +02:00
David Bruant
ca40785155 0.28.0 2025-09-18 10:41:10 +02:00
Clémence
8cc74a6fe6
Extract text inside a text:a tag (#21)
* Extract text inside a text:a tag

* Extract text inside a text:a tag and create a test

* Fix test regression

* Use example domain name for mails

* nettoyage console.log

---------

Co-authored-by: David Bruant <davidbruant@protonmail.com>
2025-09-18 10:36:59 +02:00
Clémence Fernandez
0938a97a83 bump readme version 0.27.0 2025-09-16 16:45:34 +02:00
Clémence Fernandez
8b0c1c6eb0 0.27.0 2025-09-16 16:44:27 +02:00
Clémence
d934d0dfb0
Add images (#16)
* add template

* Rename template

* Add test for insert 2 images

* image marker regex

* Ajout d'un test pour vérifier que le texte du template est bon

* WORK IN PROGRESS - trouver et évaluer la balise image

* Create OfjsImage type

* create addImageToOdtFile

* Regenerate yo odt to inspect it

* Add a draw image and a draw frame into odt file

* Test if there are two draw:image in the generated document

* Add pictures in manifest.xml to fix corrupted file

* Adapt anchor type

* Fix images aspect with ratio
2025-09-16 16:43:47 +02:00
David Bruant
a4d273793e bump readme version 2025-09-12 17:23:37 +02:00
David Bruant
9d942899ed 0.26.0 2025-09-12 17:23:15 +02:00
David Bruant
8d3d91da2f
Error thrown by mistake for {#each} inside an {#if} (#19)
* reduced test case

* test case

* adding complex test case
2025-09-12 17:22:19 +02:00
David Bruant
fbadfc7144 0.25.0 2025-07-19 14:07:54 +02:00
David Bruant
e0b2316c42 bump readme version 2025-07-19 14:07:46 +02:00
David Bruant
8eb15ad97c
fillOdtElementTemplate now takes an array of nodes as argument for the situation of calling it on the middleContent of a block with several elements so they're evaluated together (#15)
* reproducing bug

* reducing test case

* fillOdtElementTemplate now takes an array of nodes as argument for the situation of calling it on the middleContent of a block with several elements so they're evaluated together
2025-07-19 14:07:10 +02:00
David Bruant
136b240b99 0.24.0 2025-05-27 14:56:15 +02:00
David Bruant
4c67eaacd0 bump readme version 2025-05-27 14:56:10 +02:00
David Bruant
5aac86553c
Ouinon bug (#10)
* rename

* change order of removals in fillIfBlock to properly remove right/left content
2025-05-27 14:55:53 +02:00
David Bruant
9fa9c9eb62 0.23.0 2025-05-27 08:20:31 +02:00
David Bruant
38b146155c bump readme version 2025-05-27 08:20:23 +02:00
David Bruant
2d559bac5d
Better abstraction (#9)
* beginning of refactoring - if tests passing

* Beginning of passing tests for each

* Les tests each passent

* progress

* Les tests passent
2025-05-27 08:19:51 +02:00
27 changed files with 1259 additions and 547 deletions

78
package-lock.json generated
View File

@ -1,16 +1,17 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.22.0", "version": "0.30.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.22.0", "version": "0.30.0",
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.9.8", "@xmldom/xmldom": "^0.9.8",
"@zip.js/zip.js": "^2.7.57", "@zip.js/zip.js": "^2.7.57",
"ses": "^1.12.0" "image-size": "^2.0.2",
"ses": "^1.14.0"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
@ -41,10 +42,22 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@endo/cache-map": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@endo/cache-map/-/cache-map-1.1.0.tgz",
"integrity": "sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==",
"license": "Apache-2.0"
},
"node_modules/@endo/env-options": { "node_modules/@endo/env-options": {
"version": "1.1.8", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==", "integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==",
"license": "Apache-2.0"
},
"node_modules/@endo/immutable-arraybuffer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@endo/immutable-arraybuffer/-/immutable-arraybuffer-1.1.2.tgz",
"integrity": "sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
@ -2185,6 +2198,18 @@
"node": ">=10 <11 || >=12 <13 || >=14" "node": ">=10 <11 || >=12 <13 || >=14"
} }
}, },
"node_modules/image-size": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
"license": "MIT",
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
@ -3615,12 +3640,14 @@
} }
}, },
"node_modules/ses": { "node_modules/ses": {
"version": "1.12.0", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz", "resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==", "integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@endo/env-options": "^1.1.8" "@endo/cache-map": "^1.1.0",
"@endo/env-options": "^1.1.11",
"@endo/immutable-arraybuffer": "^1.1.2"
} }
}, },
"node_modules/set-blocking": { "node_modules/set-blocking": {
@ -4571,10 +4598,20 @@
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.9"
} }
}, },
"@endo/cache-map": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@endo/cache-map/-/cache-map-1.1.0.tgz",
"integrity": "sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw=="
},
"@endo/env-options": { "@endo/env-options": {
"version": "1.1.8", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==" "integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA=="
},
"@endo/immutable-arraybuffer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@endo/immutable-arraybuffer/-/immutable-arraybuffer-1.1.2.tgz",
"integrity": "sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ=="
}, },
"@jridgewell/gen-mapping": { "@jridgewell/gen-mapping": {
"version": "0.3.2", "version": "0.3.2",
@ -6129,6 +6166,11 @@
"integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==", "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==",
"dev": true "dev": true
}, },
"image-size": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="
},
"immutable": { "immutable": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
@ -7138,11 +7180,13 @@
} }
}, },
"ses": { "ses": {
"version": "1.12.0", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz", "resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==", "integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
"requires": { "requires": {
"@endo/env-options": "^1.1.8" "@endo/cache-map": "^1.1.0",
"@endo/env-options": "^1.1.11",
"@endo/immutable-arraybuffer": "^1.1.2"
} }
}, },
"set-blocking": { "set-blocking": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.22.0", "version": "0.30.0",
"type": "module", "type": "module",
"exports": "./exports.js", "exports": "./exports.js",
"files": [ "files": [
@ -21,7 +21,7 @@
"test": "ava" "test": "ava"
}, },
"repository": { "repository": {
"url": "https://github.com/odfjs/odfjs.git" "url": "https://source.netsyms.com/PostalPortal/odfjs.git"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
@ -41,6 +41,7 @@
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.9.8", "@xmldom/xmldom": "^0.9.8",
"@zip.js/zip.js": "^2.7.57", "@zip.js/zip.js": "^2.7.57",
"ses": "^1.12.0" "image-size": "^2.0.2",
"ses": "^1.14.0"
} }
} }

View File

@ -18,7 +18,7 @@ Small lib to parse/understand .odf files (.odt, .ods) in the browser and node.js
### Install ### Install
```sh ```sh
npm i https://github.com/odfjs/odfjs.git#v0.22.0 npm i https://github.com/odfjs/odfjs.git#v0.30.0
``` ```
@ -30,7 +30,7 @@ import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent}
/** /**
* @param {ArrayBuffer} odsFile - content of an .ods file * @param {ArrayBuffer} odsFile - content of an .ods file
* @return {Promise<any[]>} * @return {Promise<any[]>}
*/ */
async function getFileData(odsFile){ async function getFileData(odsFile){
return getODSTableRawContent(odsFile) return getODSTableRawContent(odsFile)
.then(tableWithoutEmptyRows) .then(tableWithoutEmptyRows)
@ -38,9 +38,9 @@ async function getFileData(odsFile){
} }
``` ```
The return value is an array of objects where The return value is an array of objects where
the **keys** are the column names in the first row and the **keys** are the column names in the first row and
the **values** are automatically converted from the .ods files (which type numbers, strings, booleans and dates) the **values** are automatically converted from the .ods files (which type numbers, strings, booleans and dates)
to the appropriate JavaScript value to the appropriate JavaScript value
@ -88,7 +88,7 @@ odf.js proposes a template syntax
In an .odt file, write the following: In an .odt file, write the following:
```txt ```txt
Hey {nom}! Hey {nom}!
Your birthdate is {dateNaissance} Your birthdate is {dateNaissance}
``` ```
@ -99,8 +99,7 @@ And then run the code:
```js ```js
import {join} from 'node:path'; import {join} from 'node:path';
import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js' import {getOdtTemplate, fillOdtTemplate} from '@odfjs/odfjs'
import {fillOdtTemplate} from '../scripts/node.js'
// replace with your template path // replace with your template path
const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt') const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt')
@ -126,6 +125,19 @@ There are also loops in the form:
They can be used to generate lists or tables in .odt files from data and a template using this syntax They can be used to generate lists or tables in .odt files from data and a template using this syntax
#### Securing calls to fillOdtTemplate
`fillOdtTemplate` evaluate arbitrary JavaScript code in `{#each <collection> as élément}` and `{#if <condition>}` and in `{<expression>}`
By default, `fillOdtTemplate` limits access to global functions to only ECMAScript defaults via the use of [ses' Compartment](https://www.npmjs.com/package/ses#compartment), this prevents naïve data exfiltration
However, `fillOdtTemplate` is vulnerable to [prototype pollution](https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html) inside template code. Two main ways to be secure are:
- control the set of possible templates
- call ses' `lockdown` which freezes Javascript intrinsics before calling `fillOdtTemplate` (this may lead to incompatibilities)
### Demo ### Demo
https://odfjs.github.io/odfjs/ https://odfjs.github.io/odfjs/
@ -146,4 +158,3 @@ npm run dev
I hope to be credited for the work on this repo I hope to be credited for the work on this repo
Everything written by me and contributors to this repo is licenced under **CC0 1.0 (Public Domain)** Everything written by me and contributors to this repo is licenced under **CC0 1.0 (Public Domain)**

View File

@ -42,6 +42,51 @@ export function traverse(node, visit) {
visit(node); visit(node);
} }
/**
*
* @param {Node} node1
* @param {Node} node2
* @returns {Node}
*/
export function findCommonAncestor(node1, node2) {
const ancestors1 = getAncestors(node1);
const ancestors2 = new Set(getAncestors(node2));
for(const ancestor of ancestors1) {
if(ancestors2.has(ancestor)) {
return ancestor;
}
}
throw new Error(`node1 and node2 do not have a common ancestor`)
}
/**
* returns ancestors youngest first, oldest last
*
* @param {Node} node
* @param {Node} [until]
* @returns {Node[]}
*/
export function getAncestors(node, until = undefined) {
const ancestors = [];
let current = node;
while(current && current !== until) {
ancestors.push(current);
current = current.parentNode;
}
if(current === until){
ancestors.push(until);
}
return ancestors;
}
export { export {
DOMParser, DOMParser,
XMLSerializer, XMLSerializer,

View File

@ -6,11 +6,15 @@ import {serializeToString, createDocument} from './DOMUtils.js'
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */ /** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
const stylesXml = `<?xml version="1.0" encoding="UTF-8"?> const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles <office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
office:version="1.2"> office:version="1.2">
<office:styles/> <office:styles>
<style:style style:name="boldcell" style:family="table-cell">
<style:text-properties fo:font-weight="bold"/>
</style:style>
</office:styles>
<office:automatic-styles/> <office:automatic-styles/>
<office:master-styles/> <office:master-styles/>
</office:document-styles>`; </office:document-styles>`;
@ -27,11 +31,11 @@ const manifestXml = `<?xml version="1.0" encoding="UTF-8"?>
* @param {Map<SheetName, SheetRawContent>} sheetsData * @param {Map<SheetName, SheetRawContent>} sheetsData
* @returns {Promise<ArrayBuffer>} * @returns {Promise<ArrayBuffer>}
*/ */
export async function createOdsFile(sheetsData) { export async function createOdsFile(sheetsData, currencyData = null) {
// Create a new zip writer // Create a new zip writer
const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet')); const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet'));
// The “mimetype” file shall be the first file of the zip file. // 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. // 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 // https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813
zipWriter.add( zipWriter.add(
@ -45,7 +49,7 @@ export async function createOdsFile(sheetsData) {
} }
); );
const contentXml = generateContentFileXMLString(sheetsData); const contentXml = generateContentFileXMLString(sheetsData, currencyData);
zipWriter.add("content.xml", new TextReader(contentXml), {level: 9}); zipWriter.add("content.xml", new TextReader(contentXml), {level: 9});
zipWriter.add("styles.xml", new TextReader(stylesXml)); zipWriter.add("styles.xml", new TextReader(stylesXml));
@ -60,10 +64,10 @@ export async function createOdsFile(sheetsData) {
/** /**
* Generate the content.xml file with spreadsheet data * Generate the content.xml file with spreadsheet data
* @param {Map<SheetName, SheetRawContent>} sheetsData * @param {Map<SheetName, SheetRawContent>} sheetsData
* @returns {string} * @returns {string}
*/ */
function generateContentFileXMLString(sheetsData) { function generateContentFileXMLString(sheetsData, currencyData) {
const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content'); const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content');
const root = doc.documentElement; const root = doc.documentElement;
@ -75,6 +79,52 @@ function generateContentFileXMLString(sheetsData) {
root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
root.setAttribute('office:version', '1.2'); root.setAttribute('office:version', '1.2');
const styleNode = doc.createElement("office:automatic-styles");
var currencyStyleName = "currencyStyle";
if (currencyData != null) {
currencyStyleName = `currency${currencyData.currencyCode.toUpperCase()}`;
const numberStyle = doc.createElement("number:currency-style");
numberStyle.setAttribute("style:name", currencyStyleName);
const numberCurrencySymbolStyle = doc.createElement("number:currency-symbol");
numberCurrencySymbolStyle.setAttribute("number:language", "en");
numberCurrencySymbolStyle.setAttribute("number:country", currencyData.countryCode.toUpperCase());
numberCurrencySymbolStyle.textContent = currencyData.currencySymbol;
numberStyle.appendChild(numberCurrencySymbolStyle);
const numberCurrencyStyle = doc.createElement("number:number");
numberCurrencyStyle.setAttribute("number:min-integer-digits", "1");
numberCurrencyStyle.setAttribute("number:decimal-places", `${currencyData.decimalPlaces}`);
numberCurrencyStyle.setAttribute("number:min-decimal-places", `${currencyData.decimalPlaces}`);
numberCurrencyStyle.setAttribute("number:grouping", "true");
numberStyle.appendChild(numberCurrencyStyle);
styleNode.appendChild(numberStyle);
const currencyCellStyleNode = doc.createElement("style:style");
currencyCellStyleNode.setAttribute("style:name", "currencycell");
currencyCellStyleNode.setAttribute("style:family", "table-cell");
currencyCellStyleNode.setAttribute("style:data-style-name", currencyStyleName);
const currencyCellTableCellProperties = doc.createElement("style:table-cell-properties");
currencyCellStyleNode.appendChild(currencyCellTableCellProperties);
styleNode.appendChild(currencyCellStyleNode);
}
const boldCellStyleNode = doc.createElement("style:style");
boldCellStyleNode.setAttribute("style:name", "boldcell");
boldCellStyleNode.setAttribute("style:family", "table-cell");
const boldCellTextPropsNode = doc.createElement("style:text-properties");
boldCellTextPropsNode.setAttribute("fo:font-weight", "bold");
boldCellStyleNode.appendChild(boldCellTextPropsNode);
styleNode.appendChild(boldCellStyleNode);
root.appendChild(styleNode);
const bodyNode = doc.createElement('office:body'); const bodyNode = doc.createElement('office:body');
root.appendChild(bodyNode); root.appendChild(bodyNode);
@ -87,8 +137,30 @@ function generateContentFileXMLString(sheetsData) {
tableNode.setAttribute('table:name', sheetName); tableNode.setAttribute('table:name', sheetName);
spreadsheetNode.appendChild(tableNode); spreadsheetNode.appendChild(tableNode);
const columnNode = doc.createElement('table:table-column'); var columnsWidthChars = {};
tableNode.appendChild(columnNode); for (let r = 0; r < sheetData.length; r++) {
for (let c = 0; c < sheetData[r].length; c++) {
var len = ((sheetData[r][c].display ?? sheetData[r][c].value) + "").length;
if (typeof columnsWidthChars[c] == "undefined") {
columnsWidthChars[c] = len;
}
columnsWidthChars[c] = Math.max(columnsWidthChars[c], len);
}
}
for (var prop in columnsWidthChars) {
var columnNode = doc.createElement('table:table-column');
columnNode.setAttribute("table:style-name", "colwidth" + columnsWidthChars[prop]);
tableNode.appendChild(columnNode);
var columnWidthNode = doc.createElement("style:style");
columnWidthNode.setAttribute("style:name", "colwidth" + columnsWidthChars[prop]);
columnWidthNode.setAttribute("style:family", "table-column");
const columnWidthPropsNode = doc.createElement("style:table-column-properties");
columnWidthPropsNode.setAttribute("style:column-width", `${columnsWidthChars[prop] * 0.26}cm`);
columnWidthNode.appendChild(columnWidthPropsNode);
styleNode.appendChild(columnWidthNode);
}
// Iterate through rows // Iterate through rows
sheetData.forEach((row) => { sheetData.forEach((row) => {
@ -101,6 +173,10 @@ function generateContentFileXMLString(sheetsData) {
const cellType = convertCellType(cell.type); const cellType = convertCellType(cell.type);
cellNode.setAttribute('office:value-type', cellType); cellNode.setAttribute('office:value-type', cellType);
if (cell.style && cell.style == "bold") {
cellNode.setAttribute('table:style-name', "boldcell");
}
// Add value attribute based on type // Add value attribute based on type
if (cell.value !== null && cell.value !== undefined) { if (cell.value !== null && cell.value !== undefined) {
switch (cellType) { switch (cellType) {
@ -111,6 +187,14 @@ function generateContentFileXMLString(sheetsData) {
cellNode.setAttribute('office:value', cell.value.toString()); cellNode.setAttribute('office:value', cell.value.toString());
cellNode.setAttribute('office:value-type', 'percentage'); cellNode.setAttribute('office:value-type', 'percentage');
break; break;
case 'currency':
cellNode.setAttribute('office:value', cell.value.toString());
cellNode.setAttribute('office:value-type', 'currency');
if (currencyData != null) {
cellNode.setAttribute("table:style-name", "currencycell");
cellNode.setAttribute('office:currency', currencyData.currencyCode.toUpperCase());
}
break;
case 'date': case 'date':
cellNode.setAttribute('office:date-value', cell.value.toString()); cellNode.setAttribute('office:date-value', cell.value.toString());
break; break;
@ -126,7 +210,11 @@ function generateContentFileXMLString(sheetsData) {
if (cellType !== 'string') { if (cellType !== 'string') {
const textNode = doc.createElement('text:p'); const textNode = doc.createElement('text:p');
textNode.textContent = cell.value.toString(); if (typeof cell.display != "undefined") {
textNode.textContent = cell.display.toString();
} else {
textNode.textContent = cell.value.toString();
}
cellNode.appendChild(textNode); cellNode.appendChild(textNode);
} }
} }
@ -141,7 +229,7 @@ function generateContentFileXMLString(sheetsData) {
/** /**
* Convert cell type to OpenDocument format type * Convert cell type to OpenDocument format type
* @param {SheetCellRawContent['type']} type * @param {SheetCellRawContent['type']} type
* @returns {SheetCellRawContent['type']} * @returns {SheetCellRawContent['type']}
*/ */
function convertCellType(type) { function convertCellType(type) {

View File

@ -7,7 +7,7 @@ import {parseXML, Node} from '../../DOMUtils.js'
* @param {ODTFile} odtFile * @param {ODTFile} odtFile
* @returns {Promise<Document>} * @returns {Promise<Document>}
*/ */
async function getContentDocument(odtFile) { export async function getContentDocument(odtFile) {
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtFile))); const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtFile)));
const entries = await reader.getEntries(); const entries = await reader.getEntries();

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,10 @@ import prepareTemplateDOMTree from './prepareTemplateDOMTree.js';
import 'ses' import 'ses'
import fillOdtElementTemplate from './fillOdtElementTemplate.js'; import fillOdtElementTemplate from './fillOdtElementTemplate.js';
lockdown();
/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */ /** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */
/** @import {ODFManifest} from '../manifest.js' */ /** @import {ODFManifest, ODFManifestFileEntry} from '../manifest.js' */
/** @import {OdfjsImage} from '../../types.js' */
/** @typedef {ArrayBuffer} ODTFile */ /** @typedef {ArrayBuffer} ODTFile */
@ -23,11 +22,12 @@ const ODTMimetype = 'application/vnd.oasis.opendocument.text'
* *
* @param {Document} document * @param {Document} document
* @param {Compartment} compartment * @param {Compartment} compartment
* @param {(OdfjsImage) => string} addImageToOdtFile
* @returns {void} * @returns {void}
*/ */
function fillOdtDocumentTemplate(document, compartment) { function fillOdtDocumentTemplate(document, compartment, addImageToOdtFile) {
prepareTemplateDOMTree(document) prepareTemplateDOMTree(document)
fillOdtElementTemplate(document, compartment) fillOdtElementTemplate(document, compartment, addImageToOdtFile)
} }
@ -64,6 +64,21 @@ export default async function fillOdtTemplate(odtTemplate, data) {
/** @type {{filename: string, content: Reader, options?: ZipWriterAddDataOptions}[]} */ /** @type {{filename: string, content: Reader, options?: ZipWriterAddDataOptions}[]} */
const zipEntriesToAdd = [] const zipEntriesToAdd = []
/** @type {ODFManifestFileEntry[]} */
const newManifestEntries = []
/**
* Return href
* @param {OdfjsImage} odfjsImage
* @returns {string}
*/
function addImageToOdtFile(odfjsImage) {
// console.log({odfjsImage})
const filename = `Pictures/${odfjsImage.fileName}`
zipEntriesToAdd.push({content: new Uint8ArrayReader(new Uint8Array(odfjsImage.content)), filename})
newManifestEntries.push({fullPath: filename, mediaType: odfjsImage.mediaType})
return filename
}
// Parcourir chaque entrée du fichier ODT // Parcourir chaque entrée du fichier ODT
for await(const entry of entries) { for await(const entry of entries) {
@ -96,13 +111,15 @@ export default async function fillOdtTemplate(odtTemplate, data) {
// @ts-ignore // @ts-ignore
const contentXml = await entry.getData(new TextWriter()); const contentXml = await entry.getData(new TextWriter());
const contentDocument = parseXML(contentXml); const contentDocument = parseXML(contentXml);
const compartment = new Compartment({ const compartment = new Compartment({
globals: data, globals: data,
__options__: true __options__: true
}) })
fillOdtDocumentTemplate(contentDocument, compartment) fillOdtDocumentTemplate(contentDocument, compartment, addImageToOdtFile)
const updatedContentXml = serializeToString(contentDocument) const updatedContentXml = serializeToString(contentDocument)
@ -138,6 +155,9 @@ export default async function fillOdtTemplate(odtTemplate, data) {
} }
} }
for(const {fullPath, mediaType} of newManifestEntries){
manifestFileData.fileEntries.set(fullPath, {fullPath, mediaType})
}
for(const {filename, content, options} of zipEntriesToAdd) { for(const {filename, content, options} of zipEntriesToAdd) {
await writer.add(filename, content, options); await writer.add(filename, content, options);

View File

@ -1,9 +1,10 @@
// the regexps below are shared, so they shoudn't have state (no 'g' flag) // the regexps below are shared, so they shoudn't have state (no 'g' flag)
export const variableRegex = /\{([^{#\/:]+?)\}/ export const variableRegex = /\{([^{#\/:]+?)\}/
export const imageMarkerRegex = /{#image\s+([^}]+?)\s*}/;
export const ifStartMarkerRegex = /{#if\s+([^}]+?)\s*}/; export const ifStartMarkerRegex = /{#if\s+([^}]+?)\s*}/;
export const elseMarker = '{:else}' export const elseMarker = '{:else}'
export const closingIfMarker = '{/if}' export const closingIfMarker = '{/if}'
export const eachStartMarkerRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/; export const eachStartMarkerRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/;
export const eachClosingMarker = '{/each}' export const eachClosingMarker = '{/each}'

View File

@ -1,6 +1,6 @@
//@ts-check //@ts-check
import {traverse, Node} from "../../DOMUtils.js"; import {traverse, Node, getAncestors, findCommonAncestor} from "../../DOMUtils.js";
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js' import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js'
@ -38,41 +38,7 @@ function findAllMatches(text, pattern) {
return results; return results;
} }
/**
*
* @param {Node} node1
* @param {Node} node2
* @returns {Node}
*/
function findCommonAncestor(node1, node2) {
const ancestors1 = getAncestors(node1);
const ancestors2 = new Set(getAncestors(node2));
for(const ancestor of ancestors1) {
if(ancestors2.has(ancestor)) {
return ancestor;
}
}
throw new Error(`node1 and node2 do not have a common ancestor`)
}
/**
*
* @param {Node} node
* @returns {Node[]}
*/
function getAncestors(node) {
const ancestors = [];
let current = node;
while(current) {
ancestors.push(current);
current = current.parentNode;
}
return ancestors;
}
/** /**
* text position of a node relative to a text nodes within a container * text position of a node relative to a text nodes within a container
@ -326,6 +292,8 @@ function consolidateMarkers(document){
consolidatedMarkers.push(positionedMarker) consolidatedMarkers.push(positionedMarker)
} }
} }
//console.log('consolidatedMarkers', consolidatedMarkers)
} }
} }
@ -442,6 +410,8 @@ function isolateMarkerText(document){
} }
}) })
//console.log('markerNodes', [...markerNodes].map(([node, markerType]) => [node.textContent, markerType]))
return markerNodes return markerNodes
} }

View File

@ -4,21 +4,20 @@ import { Uint8ArrayReader, ZipReader, TextWriter } from '@zip.js/zip.js';
import {parseXML} from './DOMUtils.js' import {parseXML} from './DOMUtils.js'
/** @import {Entry} from '@zip.js/zip.js'*/ /** @import {Entry} from '@zip.js/zip.js'*/
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent} from './types.js' */ /** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent, OdfjsImage} from './types.js' */
// https://dom.spec.whatwg.org/#interface-node // https://dom.spec.whatwg.org/#interface-node
const TEXT_NODE = 3 const TEXT_NODE = 3
/** /**
* *
* @param {Element} cell * @param {Element} cell
* @returns {string} * @returns {string}
*/ */
function extraxtODSCellText(cell) { function extraxtODSCellText(cell) {
let text = ''; let text = '';
const childNodes = cell.childNodes; const childNodes = cell.childNodes;
for (const child of Array.from(childNodes)) { for (const child of Array.from(childNodes)) {
if (child.nodeType === TEXT_NODE) { if (child.nodeType === TEXT_NODE) {
// Direct text node, append the text directly // Direct text node, append the text directly
@ -34,13 +33,15 @@ function extraxtODSCellText(cell) {
text += pChild.nodeValue; // Append text inside <text:p> text += pChild.nodeValue; // Append text inside <text:p>
} else if (pChild.nodeName === 'text:line-break') { } else if (pChild.nodeName === 'text:line-break') {
text += '\n'; // Append newline for <text:line-break /> text += '\n'; // Append newline for <text:line-break />
} else if (pChild.nodeName === 'text:a' || pChild.nodeName === 'text:span') {
text += pChild.textContent
} }
} }
} else if (child.nodeName === 'text:line-break') { } else if (child.nodeName === 'text:line-break') {
text += '\n'; // Append newline for <text:line-break /> directly under <table:table-cell> text += '\n'; // Append newline for <text:line-break /> directly under <table:table-cell>
} }
} }
return text.trim(); return text.trim();
} }
@ -126,7 +127,7 @@ export async function getODSTableRawContent(arrayBuffer) {
/** /**
* Converts a cell value to the appropriate JavaScript type based on its cell type. * Converts a cell value to the appropriate JavaScript type based on its cell type.
* @param {SheetCellRawContent} _ * @param {SheetCellRawContent} _
* @returns {number | boolean | string | Date} The converted value. * @returns {number | boolean | string | Date} The converted value.
*/ */
export function convertCellValue({value, type}) { export function convertCellValue({value, type}) {
@ -160,21 +161,37 @@ export function convertCellValue({value, type}) {
} }
/**
* @param {unknown} value
* @returns {value is OdfjsImage}
*/
export function isOdfjsImage(value) {
if (typeof value === 'object' && value!==null
&& "content" in value && value.content instanceof ArrayBuffer
&& "fileName" in value && typeof value.fileName === 'string'
&& "mediaType" in value && typeof value.mediaType === 'string'
) {
return true
} else {
return false
}
}
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentSheets * @param {Map<SheetName, SheetRawContent>} rawContentSheets
* @returns {Map<SheetName, ReturnType<convertCellValue>[][]>} * @returns {Map<SheetName, ReturnType<convertCellValue>[][]>}
*/ */
export function tableRawContentToValues(rawContentSheets){ export function tableRawContentToValues(rawContentSheets){
return new Map( return new Map(
[...rawContentSheets].map(([sheetName, rawContent]) => { [...rawContentSheets].map(([sheetName, rawContent]) => {
return [ return [
sheetName, sheetName,
rawContent rawContent
.map(row => row.map(c => convertCellValue(c))) .map(row => row.map(c => convertCellValue(c)))
] ]
@ -187,7 +204,7 @@ export function tableRawContentToValues(rawContentSheets){
*/ */
/** /**
* *
* @param {SheetCellRawContent} rawContentCell * @param {SheetCellRawContent} rawContentCell
* @returns {string} * @returns {string}
*/ */
@ -196,8 +213,8 @@ export function cellRawContentToStrings(rawContentCell){
} }
/** /**
* *
* @param {SheetRowRawContent} rawContentRow * @param {SheetRowRawContent} rawContentRow
* @returns {string[]} * @returns {string[]}
*/ */
export function rowRawContentToStrings(rawContentRow){ export function rowRawContentToStrings(rawContentRow){
@ -205,8 +222,8 @@ export function rowRawContentToStrings(rawContentRow){
} }
/** /**
* *
* @param {SheetRawContent} rawContentSheet * @param {SheetRawContent} rawContentSheet
* @returns {string[][]} * @returns {string[][]}
*/ */
export function sheetRawContentToStrings(rawContentSheet){ export function sheetRawContentToStrings(rawContentSheet){
@ -214,8 +231,8 @@ export function sheetRawContentToStrings(rawContentSheet){
} }
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentSheets * @param {Map<SheetName, SheetRawContent>} rawContentSheets
* @returns {Map<SheetName, string[][]>} * @returns {Map<SheetName, string[][]>}
*/ */
export function tableRawContentToStrings(rawContentSheets){ export function tableRawContentToStrings(rawContentSheets){
@ -236,16 +253,16 @@ export function tableRawContentToStrings(rawContentSheets){
/** /**
* This function expects the first row to contain string values which are used as column names * This function expects the first row to contain string values which are used as column names
* It outputs an array of objects which keys are * It outputs an array of objects which keys are
* *
* @param {SheetRawContent} rawContent * @param {SheetRawContent} rawContent
* @returns {any[]} * @returns {any[]}
*/ */
export function sheetRawContentToObjects(rawContent){ export function sheetRawContentToObjects(rawContent){
let [firstRow, ...dataRows] = rawContent let [firstRow, ...dataRows] = rawContent
/** @type {string[]} */ /** @type {string[]} */
const columns = firstRow.map((r, i) => { const columns = firstRow.map((r, i) => {
if (r.value === undefined || r.value === null || r.value === "") { if (r.value === undefined || r.value === null || r.value === "") {
return `Column ${i+1}` return `Column ${i+1}`
@ -267,8 +284,8 @@ export function sheetRawContentToObjects(rawContent){
} }
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentSheets * @param {Map<SheetName, SheetRawContent>} rawContentSheets
* @returns {Map<SheetName, any[]>} * @returns {Map<SheetName, any[]>}
*/ */
export function tableRawContentToObjects(rawContentSheets){ export function tableRawContentToObjects(rawContentSheets){
@ -295,7 +312,7 @@ export function isCellFilled({value}){
} }
/** /**
* @param {SheetRowRawContent} rawContentRow * @param {SheetRowRawContent} rawContentRow
* @returns {boolean} * @returns {boolean}
*/ */
export function isRowNotEmpty(rawContentRow){ export function isRowNotEmpty(rawContentRow){
@ -303,7 +320,7 @@ export function isRowNotEmpty(rawContentRow){
} }
/** /**
* @param {SheetRawContent} sheet * @param {SheetRawContent} sheet
* @returns {SheetRawContent} * @returns {SheetRawContent}
*/ */
export function removeEmptyRowsFromSheet(sheet){ export function removeEmptyRowsFromSheet(sheet){
@ -312,8 +329,8 @@ export function removeEmptyRowsFromSheet(sheet){
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentTable * @param {Map<SheetName, SheetRawContent>} rawContentTable
* @returns {Map<SheetName, SheetRawContent>} * @returns {Map<SheetName, SheetRawContent>}
*/ */
export function tableWithoutEmptyRows(rawContentTable){ export function tableWithoutEmptyRows(rawContentTable){
@ -322,4 +339,4 @@ export function tableWithoutEmptyRows(rawContentTable){
return [sheetName, removeEmptyRowsFromSheet(rawContent)] return [sheetName, removeEmptyRowsFromSheet(rawContent)]
}) })
) )
} }

View File

@ -10,4 +10,12 @@
/** @typedef {string} SheetName */ /** @typedef {string} SheetName */
/**
* @typedef OdfjsImage
* @prop {ArrayBuffer} content
* @prop {string} fileName
* @prop {string} mediaType
*
*/
export {} export {}

View File

@ -0,0 +1,25 @@
import test from 'ava';
import {join} from 'node:path';
import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js'
import {fillOdtTemplate, getOdtTextContent} from '../../exports.js'
test('template with {#each} inside an {#if}', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/if-then-each.odt')
const templateContent = `{#if liste_départements.length >= 2}{#each liste_départements as département}{département}, {/each} {/if}`
const data = {liste_départements : ['95', '33']}
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(), `95, 33,`)
});

View File

@ -28,7 +28,7 @@ test('basic template filling with {#each}', async t => {
const templateTextContent = await getOdtTextContent(odtTemplate) const templateTextContent = await getOdtTextContent(odtTemplate)
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
try{
const odtResult = await fillOdtTemplate(odtTemplate, data) const odtResult = await fillOdtTemplate(odtTemplate, data)
const odtResultTextContent = await getOdtTextContent(odtResult) const odtResultTextContent = await getOdtTextContent(odtResult)
@ -38,6 +38,7 @@ Radis
Jus d'orange Jus d'orange
Pâtes à lasagne (fraîches !) Pâtes à lasagne (fraîches !)
`) `)
}catch(e){console.error(e); throw e}
}); });
@ -277,7 +278,8 @@ test('template filling with text after {/each} in same text node', async t => {
Asperge, Asperge,
Betterave, Betterave,
Blette, en Printemps Blette,
en Printemps
`) `)
}); });
@ -327,24 +329,47 @@ Année
Année Année
Énergie par personne Énergie par personne
1970 1970
36252.637 36252.637
1980 1980
43328.78 43328.78
1990 1990
46971.94 46971.94
2000 2000
53147.277 53147.277
2010 2010
48062.32 48062.32
2020 2020
37859.246 37859.246
`.trim()) `.trim())
}); });
test('nested each without common ancestor for inner each', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/nested-each-without-common-ancestor-for-inner-each.odt')
const templateContent = `{#each liste_espèces_par_impact as élément}
{#each élément.liste_espèces as espèce}
{/each}
{/each}
`
const data = {
liste_espèces_par_impact: [
{}
]
}
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, ``)
});

View File

@ -157,11 +157,55 @@ test('template filling - formatted-start-each-single-paragraph', async t => {
const odtResult = await fillOdtTemplate(odtTemplate, data) const odtResult = await fillOdtTemplate(odtTemplate, data)
const odtResultTextContent = await getOdtTextContent(odtResult) const odtResultTextContent = await getOdtTextContent(odtResult)
t.deepEqual(odtResultTextContent.trim(), ` t.deepEqual(odtResultTextContent, `
37 37
38 38
39 39
`)
});
test('template filling - formatted ghost if then', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/ghost-if.odt')
const templateContent = `
Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if}
`
const data = {scientifique: {source_lumineuses: true}}
const odtTemplate = await getOdtTemplate(templatePath)
const templateTextContent = await getOdtTextContent(odtTemplate)
t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template')
let odtResult = await fillOdtTemplate(odtTemplate, data)
const odtResultTextContent = await getOdtTextContent(odtResult)
t.deepEqual(odtResultTextContent.trim(), `
Utilisation de sources lumineuses : Oui
`.trim()) `.trim())
}); });
test('template filling - formatted ghost if else', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/ghost-if.odt')
const templateContent = `
Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if}
`
const data = {scientifique: {source_lumineuses: false}}
const odtTemplate = await getOdtTemplate(templatePath)
const templateTextContent = await getOdtTextContent(odtTemplate)
t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template')
let odtResult = await fillOdtTemplate(odtTemplate, data)
const odtResultTextContent = await getOdtTextContent(odtResult)
t.deepEqual(odtResultTextContent.trim(), `
Utilisation de sources lumineuses : Non
`.trim())
});

View File

@ -6,7 +6,7 @@ import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js'
import {fillOdtTemplate, getOdtTextContent} from '../../exports.js' import {fillOdtTemplate, getOdtTextContent} from '../../exports.js'
test('basic template filling with {#if}', async t => { test('basic template filling with {#if}{:else} - then branch', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/description-nombre.odt') const templatePath = join(import.meta.dirname, '../fixtures/description-nombre.odt')
const templateContent = `Description du nombre {n} const templateContent = `Description du nombre {n}
@ -28,7 +28,27 @@ n est un grand nombre
n est un petit nombre n est un petit nombre
`) `)
});
test('basic template filling with {#if}{:else} - else branch', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/description-nombre.odt')
const templateContent = `Description du nombre {n}
{#if n<5}
n est un petit nombre
{:else}
n est un grand nombre
{/if}
`
const odtTemplate = await getOdtTemplate(templatePath)
const templateTextContent = await getOdtTextContent(odtTemplate)
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
try{
// else branch // else branch
const odtResult8 = await fillOdtTemplate(odtTemplate, {n: 8}) const odtResult8 = await fillOdtTemplate(odtTemplate, {n: 8})
const odtResult8TextContent = await getOdtTextContent(odtResult8) const odtResult8TextContent = await getOdtTextContent(odtResult8)
@ -36,12 +56,14 @@ n est un petit nombre
n est un grand nombre n est un grand nombre
`) `)
}
catch(e){console.error(e); throw e}
}); });
test('weird bug', async t => { test('complex structured if', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/left-branch-content-and-two-consecutive-ifs.odt') const templatePath = join(import.meta.dirname, '../fixtures/left-branch-content-and-two-consecutive-ifs.odt')
const templateContent = `Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if} const templateContent = `Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if}
{#if scientifique.source_lumineuses && scientifique.modalités_source_lumineuses } {#if scientifique.source_lumineuses && scientifique.modalités_source_lumineuses }

View File

@ -1,10 +1,12 @@
import test from 'ava'; import test from 'ava';
import {join} from 'node:path'; import {join} from 'node:path';
import { readFile } from 'node:fs/promises'
import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js' import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js'
import {fillOdtTemplate} from '../../exports.js' import {fillOdtTemplate, getOdtTextContent} from '../../exports.js'
import { listZipEntries } from '../helpers/zip-analysis.js'; import { listZipEntries } from '../helpers/zip-analysis.js';
import { getContentDocument } from '../../scripts/odf/odt/getOdtTextContent.js';
test('template filling preserves images', async t => { test('template filling preserves images', async t => {
@ -35,4 +37,48 @@ test('template filling preserves images', async t => {
`One zip entry of the result is expected to have a name that starts with 'Pictures/'` `One zip entry of the result is expected to have a name that starts with 'Pictures/'`
) )
})
test('insert 2 images', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/basic-image-insertion.odt')
const odtTemplate = await getOdtTemplate(templatePath)
const templateContent = `{title}
{#each photos as photo}
{#image photo}
{/each}
`
const templateTextContent = await getOdtTextContent(odtTemplate)
t.is(templateTextContent, templateContent, 'reconnaissance du template')
const photo1Path = join(import.meta.dirname, '../fixtures/pitchou-1.png')
const photo2Path = join(import.meta.dirname, '../fixtures/pitchou-2.png')
const photo1Buffer = (await readFile(photo1Path)).buffer
const photo2Buffer = (await readFile(photo2Path)).buffer
const photos = [{content: photo1Buffer, fileName: 'pitchou-1.png', mediaType: 'image/png'}, {content: photo2Buffer, fileName: 'pitchou-2.png', mediaType: 'image/png'}]
const data = {
title: 'Titre de mon projet',
photos,
}
const odtResult = await fillOdtTemplate(odtTemplate, data)
const resultEntries = await listZipEntries(odtResult)
t.is(
resultEntries.filter(entry => entry.filename.startsWith('Pictures/')).length, 2,
`Two pictures in 'Pictures/' folder are expected`
)
const odtContentDocument = await getContentDocument(odtResult)
const drawImageElements = odtContentDocument.getElementsByTagName('draw:image')
t.is(drawImageElements.length, 2, 'Two draw:image elements should be in the generated document.')
}) })

BIN
tests/fixtures/basic-image-insertion.odt vendored Normal file

Binary file not shown.

BIN
tests/fixtures/cellule avec style.ods vendored Normal file

Binary file not shown.

BIN
tests/fixtures/cellules avec emails.ods vendored Normal file

Binary file not shown.

BIN
tests/fixtures/ghost-if.odt vendored Normal file

Binary file not shown.

BIN
tests/fixtures/if-then-each.odt vendored Normal file

Binary file not shown.

BIN
tests/fixtures/pitchou-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
tests/fixtures/pitchou-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

View File

@ -55,4 +55,34 @@ Pourquoi t'aimeraient-ils seulement comme tu es ? (hein)
Si t'es pas comme eux quand t'es naturel` Si t'es pas comme eux quand t'es naturel`
t.deepEqual(feuille1[0][0].value, expectedValue) t.deepEqual(feuille1[0][0].value, expectedValue)
}); });
test('.ods cells with mails should be recognized', async t => {
const odsFileWithEmails = (await readFile('./tests/fixtures/cellules avec emails.ods')).buffer
const table = await getODSTableRawContent(odsFileWithEmails);
const feuille1 = table.get('Feuille1')
const row1 = feuille1[0]
t.deepEqual(row1[0].value, 'Nom')
t.deepEqual(row1[1].value, 'Email')
const row2 = feuille1[1]
t.deepEqual(row2[0].value, 'Dav')
t.deepEqual(row2[1].value, 'david@example.org')
const row3 = feuille1[2]
t.deepEqual(row3[0].value, 'Fanny')
t.deepEqual(row3[1].value, 'lemaildeFanny@example.com')
});
test('.ods cells with partially styled content should be recognized', async t => {
const odsFileWithStyle = (await readFile('./tests/fixtures/cellule avec style.ods')).buffer;
const table = await getODSTableRawContent(odsFileWithStyle);
const feuille1 = table.get('Feuille1');
const row1 = feuille1[0];
t.deepEqual(row1[0].value, 'Toto titi');
});

View File

@ -1,31 +1,28 @@
import {writeFile} from 'node:fs/promises' import {writeFile, readFile} from 'node:fs/promises'
import {join} from 'node:path'; import {join} from 'node:path';
import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js' import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js'
import {fillOdtTemplate} from '../exports.js' import {fillOdtTemplate} from '../exports.js'
/* /*
const templatePath = join(import.meta.dirname, '../tests/data/template-anniversaire.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/template-anniversaire.odt')
const data = { const data = {
nom: 'David Bruant', nom: 'David Bruant',
dateNaissance: '8 mars 1987' dateNaissance: '8 mars 1987'
} }
*/ */
/*const templatePath = join(import.meta.dirname, '../tests/fixtures/enum-courses.odt')
/*
const templatePath = join(import.meta.dirname, '../tests/data/liste-courses.odt')
const data = { const data = {
listeCourses : [ listeCourses : [
'Radis', 'Radis',
`Jus d'orange`, `Jus d'orange`,
'Pâtes à lasagne (fraîches !)' 'Pâtes à lasagne (fraîches !)'
] ]
} }*/
*/
/* /*
const templatePath = join(import.meta.dirname, '../tests/data/liste-fruits-et-légumes.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/liste-fruits-et-légumes.odt')
const data = { const data = {
fruits : [ fruits : [
'Pastèque 🍉', 'Pastèque 🍉',
@ -40,7 +37,7 @@ const data = {
}*/ }*/
/* /*
const templatePath = join(import.meta.dirname, '../tests/data/légumes-de-saison.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/légumes-de-saison.odt')
const data = { const data = {
légumesSaison : [ légumesSaison : [
{ {
@ -80,7 +77,7 @@ const data = {
*/ */
/* /*
const templatePath = join(import.meta.dirname, '../tests/data/tableau-simple.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/tableau-simple.odt')
const data = { const data = {
annéeConsos : [ annéeConsos : [
{ année: 1970, conso: 36252.637}, { année: 1970, conso: 36252.637},
@ -95,17 +92,55 @@ const data = {
/* /*
const templatePath = join(import.meta.dirname, '../tests/data/template-avec-image.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/template-avec-image.odt')
const data = { const data = {
commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶` commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶`
} }
*/ */
/*
const templatePath = join(import.meta.dirname, '../tests/fixtures/partially-formatted-variable.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/partially-formatted-variable.odt')
const data = {nombre : 37} const data = {nombre : 37}
*/
/*
const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
const data = {
saison: 'Printemps',
légumes: [
'Asperge',
'Betterave',
'Blette'
]
}
*/
// const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
// const data = {
// saison: 'Printemps',
// légumes: [
// 'Asperge',
// 'Betterave',
// 'Blette'
// ]
// }
// const templatePath = join(import.meta.dirname, '../tests/fixtures/if-then-each.odt')
// const data = {liste_départements : ['95', '33']}
const templatePath = join(import.meta.dirname, '../tests/fixtures/basic-image-insertion.odt')
const photo1Path = join(import.meta.dirname, '../tests/fixtures/pitchou-1.png')
const photo2Path = join(import.meta.dirname, '../tests/fixtures/pitchou-2.png')
const photo1Buffer = (await readFile(photo1Path)).buffer
const photo2Buffer = (await readFile(photo2Path)).buffer
const photos = [{content: photo1Buffer, fileName: 'pitchou-1.png', mediaType: 'image/png'}, {content: photo2Buffer, fileName: 'pitchou-2.png', mediaType: 'image/png'}]
const data = {
title: 'Titre de mon projet',
photos,
}
const odtTemplate = await getOdtTemplate(templatePath) const odtTemplate = await getOdtTemplate(templatePath)