Compare commits

...

34 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
David Bruant
a1e99b4519 0.22.0 2025-05-21 15:38:49 +02:00
David Bruant
4f49685acd bump readme version 2025-05-21 15:38:35 +02:00
David Bruant
03c8188bf5 add package.files field 2025-05-21 15:35:56 +02:00
David Bruant
6bb363485d 0.21.0 2025-05-21 15:10:28 +02:00
David Bruant
95a20b23c0 readme version bump 2025-05-21 15:10:25 +02:00
David Bruant
29a2429c00
Bug if 2 (#8)
* Adjust variable marker to ignore {:else}

* Getting start branch right content and end branch left content when extracting block content

* remove open if block after {/if}

* look for left/right branch content only up to common ancestor

* fix test case to new reality

* fix minor bug
2025-05-21 15:09:47 +02:00
29 changed files with 1298 additions and 492 deletions

2
.gitignore vendored
View File

@ -3,5 +3,7 @@ node_modules/
build/* build/*
.~lock* .~lock*
**/*(Copie)*
**/*(Copy)*
stats.html stats.html

78
package-lock.json generated
View File

@ -1,16 +1,17 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.20.0", "version": "0.30.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.20.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,8 +1,12 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.20.0", "version": "0.30.0",
"type": "module", "type": "module",
"exports": "./exports.js", "exports": "./exports.js",
"files": [
"exports.js",
"scripts"
],
"imports": { "imports": {
"#DOM": { "#DOM": {
"node": "./scripts/DOM/node.js", "node": "./scripts/DOM/node.js",
@ -17,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",
@ -37,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.20.0 npm i https://github.com/odfjs/odfjs.git#v0.30.0
``` ```
@ -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

@ -10,7 +10,11 @@ const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
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,7 +31,7 @@ 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'));
@ -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));
@ -63,7 +67,7 @@ export async function createOdsFile(sheetsData) {
* @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);
} }
} }

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) {
@ -97,12 +112,14 @@ export default async function fillOdtTemplate(odtTemplate, data) {
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,5 +1,6 @@
// 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}'

View File

@ -1,8 +1,9 @@
//@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'
/** /**
* *
* @param {string} text * @param {string} text
@ -37,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
@ -166,6 +133,7 @@ function consolidateMarkers(document){
] ]
for(const potentialMarkersContainer of potentialMarkersContainers) { for(const potentialMarkersContainer of potentialMarkersContainers) {
/** @type {{marker: string, index: number}[]} */
const consolidatedMarkers = [] const consolidatedMarkers = []
/** @type {Text[]} */ /** @type {Text[]} */
@ -244,13 +212,16 @@ function consolidateMarkers(document){
// Check if marker spans multiple nodes // Check if marker spans multiple nodes
if(startNode !== endNode) { if(startNode !== endNode) {
//console.log('startNode !== endNode', startNode.textContent, endNode.textContent)
const commonAncestor = findCommonAncestor(startNode, endNode) const commonAncestor = findCommonAncestor(startNode, endNode)
/** @type {Node} */
let commonAncestorStartChild = startNode let commonAncestorStartChild = startNode
while(commonAncestorStartChild.parentNode !== commonAncestor){ while(commonAncestorStartChild.parentNode !== commonAncestor){
commonAncestorStartChild = commonAncestorStartChild.parentNode commonAncestorStartChild = commonAncestorStartChild.parentNode
} }
/** @type {Node} */
let commonAncestorEndChild = endNode let commonAncestorEndChild = endNode
while(commonAncestorEndChild.parentNode !== commonAncestor){ while(commonAncestorEndChild.parentNode !== commonAncestor){
commonAncestorEndChild = commonAncestorEndChild.parentNode commonAncestorEndChild = commonAncestorEndChild.parentNode
@ -321,7 +292,10 @@ function consolidateMarkers(document){
consolidatedMarkers.push(positionedMarker) consolidatedMarkers.push(positionedMarker)
} }
} }
//console.log('consolidatedMarkers', consolidatedMarkers)
} }
} }
/** /**
@ -436,6 +410,8 @@ function isolateMarkerText(document){
} }
}) })
//console.log('markerNodes', [...markerNodes].map(([node, markerType]) => [node.textContent, markerType]))
return markerNodes return markerNodes
} }
@ -551,5 +527,4 @@ export default function prepareTemplateDOMTree(document){
isolateMarkerText(document) isolateMarkerText(document)
// after isolateMarkerText, each marker is in exactly one text node // after isolateMarkerText, each marker is in exactly one text node
// (markers are separated from text that was before or after in the same text node) // (markers are separated from text that was before or after in the same text node)
} }

View File

@ -4,7 +4,7 @@ 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
@ -18,7 +18,6 @@ const TEXT_NODE = 3
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,6 +33,8 @@ 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') {
@ -160,6 +161,22 @@ 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
}
}

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
`) `)
}); });
@ -342,3 +344,32 @@ Année
`.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}
@ -29,6 +29,26 @@ 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,7 +56,36 @@ n est un petit nombre
n est un grand nombre n est un grand nombre
`) `)
}
catch(e){console.error(e); throw e}
}); });
test('complex structured if', async t => {
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}
{#if scientifique.source_lumineuses && scientifique.modalités_source_lumineuses }
Modalités dutilisation de sources lumineuses : {scientifique.modalités_source_lumineuses}
{/if}
`
const data = {
scientifique: {
source_lumineuses: false,
//modalités_source_lumineuses: 'lampes torches'
}
}
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(), `Utilisation de sources lumineuses : Non`)
});

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 => {
@ -36,3 +38,47 @@ test('template filling preserves images', async t => {
) )
}) })
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.

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

@ -56,3 +56,33 @@ 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)