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
``` ```
@ -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,9 +137,31 @@ 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 = {};
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); 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) => {
const rowNode = doc.createElement('table:table-row'); const rowNode = doc.createElement('table:table-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');
if (typeof cell.display != "undefined") {
textNode.textContent = cell.display.toString();
} else {
textNode.textContent = cell.value.toString(); 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();

View File

@ -1,5 +1,8 @@
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, imageMarkerRegex, variableRegex} from './markers.js'
import {isOdfjsImage} from "../../shared.js"
import imageSize from "image-size";
/** @import {OdfjsImage} from "../../types.js" */
/** /**
* @typedef TextPlaceToFill * @typedef TextPlaceToFill
@ -7,6 +10,357 @@ import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, if
* @property {() => void} fill * @property {() => void} fill
*/ */
class TemplateDOMBranch{
/** @type {Node} */
#branchBaseNode
/** @type {Node} */
#leafNode
// ancestors with this.#ancestors[0] === this.#branchBaseNode and this.#ancestors.at(-1) === this.#leafNode
/** @type {Node[]} */
#ancestors
/**
*
* @param {Node} branchBaseNode
* @param {Node} leafNode
*/
constructor(branchBaseNode, leafNode){
this.#branchBaseNode = branchBaseNode
this.#leafNode = leafNode
this.#ancestors = getAncestors(this.#leafNode, this.#branchBaseNode).reverse()
}
/**
*
* @param {number} n
* @returns {Node | undefined}
*/
at(n){
return this.#ancestors.at(n)
}
removeLeafAndEmptyAncestors(){
//this.logBranch('[removeLeafAndEmptyAncestors] branch at the start')
// it may happen (else marker of if/else/endif) that the leaf was already removed as part of another block
// so before removing anything, let's update #ancestors and #leaf
this.#ancestors.every((ancestor, i) => {
if(!ancestor.parentNode){
// ancestor already removed from tree
this.#ancestors = this.#ancestors.slice(0, i)
return false;
}
return true // continue
})
this.#leafNode = this.#ancestors.at(-1)
//this.logBranch('[removeLeafAndEmptyAncestors] after adjusting this.#ancestors')
//console.log('removeLeafAndEmptyAncestors', this.#startNode.textContent)
let nextLeaf
if(this.#leafNode !== this.#branchBaseNode){
nextLeaf = this.#leafNode.parentNode
//console.log('nextLeaf', !!nextLeaf)
nextLeaf.removeChild(this.#leafNode)
this.#leafNode = nextLeaf
}
while(this.#leafNode !== this.#branchBaseNode &&
(this.#leafNode.textContent === null || this.#leafNode.textContent.trim() === ''))
{
nextLeaf = this.#leafNode.parentNode
this.#leafNode.parentNode.removeChild(this.#leafNode)
this.#leafNode = nextLeaf
}
this.#ancestors = getAncestors(this.#leafNode, this.#branchBaseNode).reverse()
}
/**
*
* @param {number} [startIndex]
*/
removeRightContent(startIndex = 0){
//console.log('[removeRightContent]', startIndex, this.#ancestors.slice(startIndex).length)
for(const branchNode of this.#ancestors.slice(startIndex)){
//console.log('[removeRightContent]', branchNode.nodeType, branchNode.nodeName)
let toRemove = branchNode.nextSibling
while(toRemove){
const toRemoveNext = toRemove.nextSibling
toRemove.parentNode.removeChild(toRemove)
toRemove = toRemoveNext
}
}
}
/**
*
* @param {number} [startIndex]
*/
removeLeftContent(startIndex = 0){
for(const branchNode of this.#ancestors.slice(startIndex)){
let toRemove = branchNode.previousSibling
while(toRemove){
const toRemoveNext = toRemove.previousSibling
toRemove.parentNode.removeChild(toRemove)
toRemove = toRemoveNext
}
}
}
/**
*
* @returns {number[]}
*/
getBranchPath(){
//console.log('[getBranchPath]', this.#branchBaseNode.nodeName, this.#branchBaseNode.textContent)
//console.log('[getBranchPath] leaf', this.#leafNode.nodeName, this.#leafNode.textContent)
/** @type {ReturnType<typeof TemplateDOMBranch.prototype.getBranchPath>} */
const pathFromLeafToBase = [];
let currentNode = this.#leafNode
let currentNodeParent = currentNode.parentNode
while(currentNodeParent){
//console.log('[getBranchPath] currentNodeParent', currentNodeParent.nodeName)
//console.log('[getBranchPath] looking for currentNode', currentNode.nodeName, currentNode.textContent)
//console.log('[getBranchPath] currentNodeParent.childNodes.length', currentNodeParent.childNodes.length)
/*console.log('[getBranchPath] currentNodeParent.childNodes', Array.from(currentNodeParent.childNodes)
.map(n => `${n.nodeName} - ${n.textContent}`)
)*/
const index = Array.from(currentNodeParent.childNodes).indexOf(currentNode)
//console.log('[getBranchPath] indexOf', index)
if(index === -1){
throw new Error(`Could not find currentNode in currentNodeParent's childNodes`)
}
pathFromLeafToBase.push(index)
//console.log('[getBranchPath] currentNodeParent and index', currentNodeParent.nodeName, index)
if(currentNodeParent === this.#ancestors[0]){
break; // path is fnished
}
else{
currentNode = currentNodeParent
currentNodeParent = currentNode.parentNode
}
}
//@ts-expect-error ES2023
return pathFromLeafToBase.toReversed()
}
logBranch(message){
console.group('[TemplateDOMBranch] Showing branch')
console.log(message)
for(const node of this.#ancestors){
console.log('branch node', node.nodeType, node.nodeName, node.nodeType === node.TEXT_NODE ? node.textContent : '')
}
console.groupEnd()
}
}
class TemplateBlock{
/** @type {Element | Document | DocumentFragment} */
#commonAncestor;
/** @type {TemplateDOMBranch} */
startBranch;
/** @type {TemplateDOMBranch} */
endBranch;
/** @type {Node[]} */
#middleContent;
/**@type {any} */
#addImageToOdtFile;
/**
*
* @param {Node} startNode
* @param {Node} endNode
* @param {(OdfjsImage) => string} addImageToOdtFile
*/
constructor(startNode, endNode, addImageToOdtFile){
this.#addImageToOdtFile = addImageToOdtFile
// @ts-expect-error xmldom.Node
this.#commonAncestor = findCommonAncestor(startNode, endNode)
//console.log('create start branch')
this.startBranch = new TemplateDOMBranch(this.#commonAncestor, startNode)
//console.log('create end branch')
this.endBranch = new TemplateDOMBranch(this.#commonAncestor, endNode)
this.#middleContent = []
let content = this.startBranch.at(1).nextSibling
while(content && content !== this.endBranch.at(1)){
this.#middleContent.push(content)
content = content.nextSibling
}
//console.group('\n== TemplateBlock ==')
//this.startBranch.logBranch('startBranch')
//console.log('middleContent', this.#middleContent.map(n => n.textContent).join(''))
//this.endBranch.logBranch('endBranch')
//console.log('common ancestor', this.#commonAncestor.nodeName, '\n')
//console.groupEnd()
}
removeMarkersAndEmptyAncestors(){
//console.log('[removeMarkersAndEmptyAncestors]', this.#commonAncestor.textContent)
this.startBranch.removeLeafAndEmptyAncestors()
this.endBranch.removeLeafAndEmptyAncestors()
//console.log('[removeMarkersAndEmptyAncestors] after', this.#commonAncestor.textContent)
}
/**
*
* @param {Compartment} compartement
*/
fillBlockContentTemplate(compartement){
//console.log('[fillBlockContentTemplate] start')
const startChild = this.startBranch.at(1)
if(startChild /*&& startChild !== */){
//console.log('[fillBlockContentTemplate] startChild', startChild.nodeName, startChild.textContent)
fillOdtElementTemplate(startChild, compartement, this.#addImageToOdtFile)
}
//console.log('[fillBlockContentTemplate] after startChild')
// if content consists of several parts of an {#each}{/each}
// when arriving to the {/each}, it will be alone (and imbalanced)
// and will trigger an error
fillOdtElementTemplate(Array.from(this.#middleContent), compartement, this.#addImageToOdtFile)
//console.log('[fillBlockContentTemplate] after middleContent')
const endChild = this.endBranch.at(1)
//console.log('fillBlockContentTemplate] [endBranch]')
//this.endBranch.logBranch('endBranch')
if(endChild){
//console.log('[fillBlockContentTemplate] endChild', endChild.nodeName, endChild.textContent)
fillOdtElementTemplate(endChild, compartement, this.#addImageToOdtFile)
}
//console.log('[fillBlockContentTemplate] after endChild')
//console.log('[fillBlockContentTemplate] end')
}
removeContent(){
this.startBranch.removeRightContent(2)
for(const content of this.#middleContent){
content.parentNode.removeChild(content)
}
this.endBranch.removeLeftContent(2)
}
/**
* @returns {TemplateBlock}
*/
cloneAndAppendAfter(){
//console.log('[cloneAndAppendAfter]')
const clonedPieces = []
let startBranchClone;
let endBranchClone;
for(const sibling of [this.startBranch.at(1), ...this.#middleContent, this.endBranch.at(1)]){
if(sibling){
const siblingClone = sibling.cloneNode(true)
clonedPieces.push(siblingClone)
if(sibling === this.startBranch.at(1))
startBranchClone = siblingClone
if(sibling === this.endBranch.at(1))
endBranchClone = siblingClone
}
}
let startChildPreviousSiblingsCount = 0
let previousSibling = this.startBranch.at(1).previousSibling
while(previousSibling){
startChildPreviousSiblingsCount = startChildPreviousSiblingsCount + 1
previousSibling = previousSibling.previousSibling
}
const startBranchPathFromBaseToLeaf = this.startBranch.getBranchPath().slice(1)
const endBranchPathFromBaseToLeaf = this.endBranch.getBranchPath().slice(1)
//console.log('startBranchClone', !!startBranchClone)
//console.log('startBranchPathFromBaseToLeaf', startBranchPathFromBaseToLeaf)
let startLeafCloneNode
{
let node = startBranchClone
for(let pathIndex of startBranchPathFromBaseToLeaf){
//console.log('[startLeafCloneNode] node.childNodes.length', node.childNodes.length)
//console.log('[startLeafCloneNode] pathIndex', pathIndex)
node = node.childNodes[pathIndex]
}
startLeafCloneNode = node
}
//console.log('endBranchClone', !!endBranchClone)
//console.log('endBranchPathFromBaseToLeaf', endBranchPathFromBaseToLeaf)
let endLeafCloneNode
{
let node = endBranchClone
for(let pathIndex of endBranchPathFromBaseToLeaf){
//console.log('[endLeafCloneNode] node.childNodes.length', node.childNodes.length)
//console.log('[endLeafCloneNode] pathIndex', pathIndex)
node = node.childNodes[pathIndex]
}
endLeafCloneNode = node
}
let insertBeforePoint = this.endBranch.at(1) && this.endBranch.at(1).nextSibling
if(insertBeforePoint){
for(const node of clonedPieces){
this.#commonAncestor.insertBefore(node, insertBeforePoint)
}
}
else{
for(const node of clonedPieces){
this.#commonAncestor.appendChild(node)
}
}
return new TemplateBlock(startLeafCloneNode, endLeafCloneNode, this.#addImageToOdtFile)
}
}
/** /**
* @param {string} str * @param {string} str
@ -71,130 +425,6 @@ function findPlacesToFillInString(str, compartment) {
} }
/**
* Content between blockStartNode and blockEndNode is extracted to a documentFragment
* The original document is modified because nodes are removed from it to be part of the returned documentFragment
*
* startChild and endChild are ancestors of, respectively, blockStartNode and blockEndNode
* and startChild.parentNode === endChild.parentNode
*
* @precondition blockStartNode needs to be before blockEndNode in document order
*
* @param {Node} blockStartNode
* @param {Node} blockEndNode
* @returns {{startChild: Node, endChild:Node, content: DocumentFragment}}
*/
function extractBlockContent(blockStartNode, blockEndNode) {
//console.log('[extractBlockContent] blockStartNode', blockStartNode.textContent)
//console.log('[extractBlockContent] blockEndNode', blockEndNode.textContent)
// find common ancestor of blockStartNode and blockEndNode
let commonAncestor
let startAncestor = blockStartNode
let endAncestor = blockEndNode
// ancestries in order of deepest first, closest to root last
const startAncestry = new Set([startAncestor])
const endAncestry = new Set([endAncestor])
while(!startAncestry.has(endAncestor) && !endAncestry.has(startAncestor)) {
if(startAncestor.parentNode) {
startAncestor = startAncestor.parentNode
startAncestry.add(startAncestor)
}
if(endAncestor.parentNode) {
endAncestor = endAncestor.parentNode
endAncestry.add(endAncestor)
}
}
if(startAncestry.has(endAncestor)) {
commonAncestor = endAncestor
}
else {
commonAncestor = startAncestor
}
const startAncestryToCommonAncestor = [...startAncestry].slice(0, [...startAncestry].indexOf(commonAncestor))
const endAncestryToCommonAncestor = [...endAncestry].slice(0, [...endAncestry].indexOf(commonAncestor))
// direct children of commonAncestor in the branch or blockStartNode and blockEndNode respectively
const startChild = startAncestryToCommonAncestor.at(-1)
const endChild = endAncestryToCommonAncestor.at(-1)
//console.log('[extractBlockContent] startChild', startChild.textContent)
//console.log('[extractBlockContent] endChild', endChild.textContent)
// Extract DOM content in a documentFragment
/** @type {DocumentFragment} */
const contentFragment = blockStartNode.ownerDocument.createDocumentFragment()
/** @type {Element[]} */
const blockContent = []
// get start branch "right" content
for(const startAncestor of startAncestryToCommonAncestor){
if(startAncestor === startChild)
break;
let sibling = startAncestor.nextSibling
while(sibling) {
blockContent.push(sibling)
sibling = sibling.nextSibling;
}
}
let sibling = startChild.nextSibling
while(sibling !== endChild) {
blockContent.push(sibling)
sibling = sibling.nextSibling;
}
// get end branch "left" content
for(const endAncestor of [...endAncestryToCommonAncestor].reverse()){
if(endAncestor === endChild)
continue; // already taken care of
let sibling = endAncestor.previousSibling
const reversedBlockContentContribution = []
while(sibling) {
reversedBlockContentContribution.push(sibling)
sibling = sibling.previousSibling;
}
const blockContentContribution = reversedBlockContentContribution.reverse()
blockContent.push(...blockContentContribution)
if(endAncestor === blockEndNode)
break;
}
//console.log('blockContent', blockContent.map(n => n.textContent))
for(const sibling of blockContent) {
sibling.parentNode?.removeChild(sibling)
contentFragment.appendChild(sibling)
}
//console.log('extractBlockContent contentFragment', contentFragment.textContent)
return {
startChild,
endChild,
content: contentFragment
}
}
@ -205,69 +435,55 @@ function extractBlockContent(blockStartNode, blockEndNode) {
* @param {Node} ifClosingMarkerNode * @param {Node} ifClosingMarkerNode
* @param {string} ifBlockConditionExpression * @param {string} ifBlockConditionExpression
* @param {Compartment} compartment * @param {Compartment} compartment
* // TODO type,addImageToOdtFile
*/ */
function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment) { function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment, addImageToOdtFile) {
//const docEl = ifOpeningMarkerNode.ownerDocument.documentElement
const conditionValue = compartment.evaluate(ifBlockConditionExpression) const conditionValue = compartment.evaluate(ifBlockConditionExpression)
let startChild /** @type {TemplateBlock | undefined} */
let endChild let thenTemplateBlock
/** @type {TemplateBlock | undefined} */
let markerNodes = new Set() let elseTemplateBlock
let chosenFragment
if(ifElseMarkerNode) { if(ifElseMarkerNode) {
const { /*console.log('before first extract',
startChild: startIfThenChild, ifOpeningMarkerNode.childNodes.length, ifOpeningMarkerNode.textContent,
endChild: endIfThenChild, ifElseMarkerNode.childNodes.length, ifElseMarkerNode.textContent
content: thenFragment )*/
} = extractBlockContent(ifOpeningMarkerNode, ifElseMarkerNode)
const { thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifElseMarkerNode, addImageToOdtFile)
startChild: startIfElseChild, elseTemplateBlock = new TemplateBlock(ifElseMarkerNode, ifClosingMarkerNode, addImageToOdtFile)
endChild: endIfElseChild,
content: elseFragment
} = extractBlockContent(ifElseMarkerNode, ifClosingMarkerNode)
chosenFragment = conditionValue ? thenFragment : elseFragment
startChild = startIfThenChild
endChild = endIfElseChild
markerNodes
.add(startIfThenChild).add(endIfThenChild)
.add(startIfElseChild).add(endIfElseChild)
} }
else { else {
const { thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifClosingMarkerNode, addImageToOdtFile)
startChild: startIfThenChild,
endChild: endIfThenChild,
content: thenFragment
} = extractBlockContent(ifOpeningMarkerNode, ifClosingMarkerNode)
chosenFragment = conditionValue ? thenFragment : undefined
startChild = startIfThenChild
endChild = endIfThenChild
markerNodes
.add(startIfThenChild).add(endIfThenChild)
} }
if(chosenFragment) { if(conditionValue) {
fillOdtElementTemplate( if(elseTemplateBlock){
chosenFragment, elseTemplateBlock.removeContent()
compartment
)
endChild.parentNode.insertBefore(chosenFragment, endChild)
} }
for(const markerNode of markerNodes) { thenTemplateBlock.removeMarkersAndEmptyAncestors()
try { if(elseTemplateBlock){
// may throw if node already out of tree elseTemplateBlock.removeMarkersAndEmptyAncestors()
// might happen if }
markerNode.parentNode.removeChild(markerNode)
thenTemplateBlock.fillBlockContentTemplate(compartment)
}
else{
// remove content before removing markers so that right and left content are fully removed
thenTemplateBlock.removeContent()
thenTemplateBlock.removeMarkersAndEmptyAncestors()
if(elseTemplateBlock){
elseTemplateBlock.removeMarkersAndEmptyAncestors()
}
if(elseTemplateBlock){
elseTemplateBlock.fillBlockContentTemplate(compartment)
} }
catch(e) {}
} }
} }
@ -280,113 +496,119 @@ function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode,
* @param {string} itemExpression * @param {string} itemExpression
* @param {Node} endNode * @param {Node} endNode
* @param {Compartment} compartment * @param {Compartment} compartment
* // TODO type addImageToOdtFile
*/ */
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment) { function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment, addImageToOdtFile) {
//console.log('fillEachBlock', iterableExpression, itemExpression) //console.log('fillEachBlock', iterableExpression, itemExpression)
const {startChild, endChild, content: repeatedFragment} = extractBlockContent(startNode, endNode) const docEl = startNode.ownerDocument.documentElement
//console.log('[fillEachBlock] docEl', docEl.textContent)
const repeatedTemplateBlock = new TemplateBlock(startNode, endNode, addImageToOdtFile)
// Find the iterable in the data // Find the iterable in the data
// PPP eventually, evaluate the expression as a JS expression
let iterable = compartment.evaluate(iterableExpression) let iterable = compartment.evaluate(iterableExpression)
if(!iterable || typeof iterable[Symbol.iterator] !== 'function') { if(!iterable || typeof iterable[Symbol.iterator] !== 'function') {
// when there is no iterable, silently replace with empty array // when there is no iterable, silently replace with empty array
iterable = [] iterable = []
} }
let firstItemFirstChild // convert to array to know the size and know which element is last
let lastItemLastChild if(!Array.isArray(iterable))
iterable = [...iterable]
// create each loop result
// using a for-of loop to accept all iterable values
for(const item of iterable) {
/** @type {DocumentFragment} */ if(iterable.length === 0){
// @ts-ignore repeatedTemplateBlock.removeMarkersAndEmptyAncestors()
const itemFragment = repeatedFragment.cloneNode(true) repeatedTemplateBlock.removeContent()
}
else{
let nextTemplateBlock = repeatedTemplateBlock
iterable.forEach((item, i) => {
//console.log('[fillEachBlock] loop i', i, docEl.textContent)
const firstItem = i === 0
const lastItem = i === iterable.length - 1
let currentTemplateBlock = nextTemplateBlock;
//console.log('currentTemplateBlock', currentTemplateBlock.startBranch.at(0).textContent)
if(!lastItem){
nextTemplateBlock = currentTemplateBlock.cloneAndAppendAfter()
}
let insideCompartment = new Compartment({ let insideCompartment = new Compartment({
globals: Object.assign({}, compartment.globalThis, {[itemExpression]: item}), globals: Object.assign({}, compartment.globalThis, {[itemExpression]: item}),
__options__: true __options__: true
}) })
// recursive call to fillTemplatedOdtElement on itemFragment if(!firstItem){
fillOdtElementTemplate( currentTemplateBlock.startBranch.removeLeftContent(2)
itemFragment, }
insideCompartment if(!lastItem){
) //console.log('[fillEachBlock] removeRightContent')
currentTemplateBlock.endBranch.removeRightContent(2)
if(!firstItemFirstChild){
firstItemFirstChild = itemFragment.firstChild
} }
// eventually, will be set to the last item's last child //console.log('[fillEachBlock] docEl i before removeMarkers', i, docEl.textContent)
lastItemLastChild = itemFragment.lastChild currentTemplateBlock.removeMarkersAndEmptyAncestors()
//console.log('[fillEachBlock] docEl i after removeMarkers', i, docEl.textContent)
endChild.parentNode.insertBefore(itemFragment, endChild) //console.log('\nrecursive call to fillBlockContentTemplate')
currentTemplateBlock.fillBlockContentTemplate(insideCompartment)
//console.log('[fillEachBlock] docEl i after remove contents', i, docEl.textContent)
})
} }
// add before-text if any
const startNodePreviousSiblings = []
let startNodePreviousSibling = startNode.previousSibling
while(startNodePreviousSibling){
startNodePreviousSiblings.push(startNodePreviousSibling)
startNodePreviousSibling = startNodePreviousSibling.previousSibling
}
// set the array back to tree order
startNodePreviousSiblings.reverse()
if(startNodePreviousSiblings.length >= 1){
let firstItemFirstestDescendant = firstItemFirstChild
while(firstItemFirstestDescendant?.firstChild){
firstItemFirstestDescendant = firstItemFirstestDescendant.firstChild
}
for(const beforeFirstNodeElement of startNodePreviousSiblings){
firstItemFirstestDescendant?.parentNode?.insertBefore(beforeFirstNodeElement, firstItemFirstestDescendant)
}
}
// add after-text if any
const endNodeNextSiblings = []
let endNodeNextSibling = endNode.nextSibling
while(endNodeNextSibling){
endNodeNextSiblings.push(endNodeNextSibling)
endNodeNextSibling = endNodeNextSibling.nextSibling
}
if(endNodeNextSiblings.length >= 1){
let lastItemLatestDescendant = lastItemLastChild
while(lastItemLatestDescendant?.lastChild){
lastItemLatestDescendant = lastItemLatestDescendant.lastChild
}
for(const afterEndNodeElement of endNodeNextSiblings){
lastItemLatestDescendant?.parentNode?.appendChild(afterEndNodeElement)
}
}
// remove block marker elements
startChild.parentNode.removeChild(startChild)
endChild.parentNode.removeChild(endChild)
} }
/**
* @param {string} str
* @param {Compartement} compartment
* @returns { {expression: string, odfjsImage: OdfjsImage | undefined} | undefined}
*/
function findImageMarker(str, compartment) {
const imageRexExp = new RegExp(imageMarkerRegex.source, 'g');
const match = imageRexExp.exec(str)
if (match===null){
return;
}
const expression = match[1]
const value = compartment.evaluate(expression)
if (isOdfjsImage(value)) {
return { expression, odfjsImage: value}
} else {
return { expression }
}
}
const IF = ifStartMarkerRegex.source const IF = ifStartMarkerRegex.source
const EACH = eachStartMarkerRegex.source const EACH = eachStartMarkerRegex.source
/** @typedef {Element | DocumentFragment | Document} RootElementArgument */
/** /**
* *
* @param {Element | DocumentFragment | Document} rootElement * @param {RootElementArgument | RootElementArgument[]} rootElements
* @param {Compartment} compartment * @param {Compartment} compartment
* @param {(OdfjsImage) => string} addImageToOdtFile
* @returns {void} * @returns {void}
*/ */
export default function fillOdtElementTemplate(rootElement, compartment) { export default function fillOdtElementTemplate(rootElements, compartment, addImageToOdtFile) {
//console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName, rootElement.textContent)
//console.log('fillTemplatedOdtElement', rootElement.childNodes[0].childNodes.length) if(!Array.isArray(rootElements)){
rootElements = [rootElements]
}
//console.log('[fillTemplatedOdtElement]', rootElements.length, rootElements[0].nodeType, rootElements[0].nodeName, rootElements[0].textContent)
//console.log('[fillTemplatedOdtElement]', rootElement.documentElement && rootElement.documentElement.textContent)
let currentlyOpenBlocks = [] let currentlyOpenBlocks = []
@ -409,9 +631,12 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
// Traverse "in document order" // Traverse "in document order"
for(const rootElement of rootElements){
// @ts-ignore // @ts-ignore
traverse(rootElement, currentNode => { traverse(rootElement, currentNode => {
//console.log('currentlyOpenBlocks', currentlyOpenBlocks) //console.log('currentlyOpenBlocks', currentlyOpenBlocks)
//console.log('eachOpeningMarkerNode', eachOpeningMarkerNode)
const insideAnOpenBlock = currentlyOpenBlocks.length >= 1 const insideAnOpenBlock = currentlyOpenBlocks.length >= 1
@ -448,10 +673,10 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
if(isEachClosingBlock) { if(isEachClosingBlock) {
//console.log('isEachClosingBlock', isEachClosingBlock) //console.log('isEachClosingBlock', isEachClosingBlock, currentlyOpenBlocks)
if(!eachOpeningMarkerNode) if(!insideAnOpenBlock)
throw new Error(`{/each} found without corresponding opening {#each x as y}`) throw new Error('{/each} found without corresponding opening {#each x as y}')
if(currentlyOpenBlocks.at(-1) !== EACH) if(currentlyOpenBlocks.at(-1) !== EACH)
throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`) throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`)
@ -461,7 +686,11 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
// found an {#each} and its corresponding {/each} // found an {#each} and its corresponding {/each}
// execute replacement loop // execute replacement loop
fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment) //console.log('start of fillEachBlock')
fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment, addImageToOdtFile)
//console.log('end of fillEachBlock')
eachOpeningMarkerNode = undefined eachOpeningMarkerNode = undefined
eachBlockIterableExpression = undefined eachBlockIterableExpression = undefined
@ -472,6 +701,7 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
// ignore because it will be treated as part of the outer {#each} // ignore because it will be treated as part of the outer {#each}
} }
//console.log('popping currentlyOpenBlocks')
currentlyOpenBlocks.pop() currentlyOpenBlocks.pop()
} }
@ -533,7 +763,7 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
// found an {#if} and its corresponding {/if} // found an {#if} and its corresponding {/if}
// execute replacement loop // execute replacement loop
fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment) fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment, addImageToOdtFile)
ifOpeningMarkerNode = undefined ifOpeningMarkerNode = undefined
ifElseMarkerNode = undefined ifElseMarkerNode = undefined
@ -566,6 +796,54 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
const newTextNode = currentNode.ownerDocument?.createTextNode(newText) const newTextNode = currentNode.ownerDocument?.createTextNode(newText)
// @ts-ignore // @ts-ignore
currentNode.parentNode?.replaceChild(newTextNode, currentNode) currentNode.parentNode?.replaceChild(newTextNode, currentNode)
} else {
const imageMarker = findImageMarker(currentNode.data, compartment)
if (imageMarker){
//console.log({imageMarker}, "dans le if imageMarker")
if (imageMarker.odfjsImage) {
const href = addImageToOdtFile(imageMarker.odfjsImage)
const newImageNode = currentNode.ownerDocument?.createElement("draw:image")
newImageNode.setAttribute("xlink:href", href)
newImageNode.setAttribute("xlink:type", "simple")
newImageNode.setAttribute("xlink:show", "embed")
newImageNode.setAttribute("xlink:actuate", "onLoad")
newImageNode.setAttribute("draw:mime-type", imageMarker.odfjsImage.mediaType)
const newFrameNode = currentNode.ownerDocument?.createElement('draw:frame')
newFrameNode.setAttribute("text:anchor-type", "as-char")
const buffer = new Uint8Array(imageMarker.odfjsImage.content)
const dimensions = imageSize(buffer)
const MAX_WIDTH = 10 // cm
const MAX_HEIGHT = 10 // cm
let width;
let height;
if(dimensions.width > dimensions.height){
// image in landscape
width = MAX_WIDTH;
height = width*dimensions.height/dimensions.width
}
else{
// image in portrait
height = MAX_HEIGHT;
width = height*dimensions.width/dimensions.height
}
newFrameNode.setAttribute("svg:width", `${width}cm`)
newFrameNode.setAttribute("svg:height", `${height}cm`)
newFrameNode.appendChild(newImageNode)
currentNode.parentNode?.replaceChild(newFrameNode, currentNode)
} else {
throw new Error(`No valid OdfjsImage value has been found for expression: ${imageMarker.expression}`)
}
}
} }
} }
} }
@ -592,4 +870,6 @@ export default function fillOdtElementTemplate(rootElement, compartment) {
} }
} }
}) })
}
} }

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,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,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
`) `)
}); });
@ -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}
@ -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,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 => {
@ -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.

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)