Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6e09a0361 | |||
| 3d98e41d2b | |||
| 7c87d3220d | |||
| 8b60ceb39c | |||
|
|
c677c7267b | ||
|
|
415a26e3f1 | ||
|
|
90b97e23e9 | ||
|
|
02d5338634 | ||
|
|
542183a593 | ||
|
|
a31f57026d | ||
|
|
98817e9d34 | ||
|
|
ca40785155 | ||
|
|
8cc74a6fe6 | ||
|
|
0938a97a83 | ||
|
|
8b0c1c6eb0 | ||
|
|
d934d0dfb0 | ||
|
|
a4d273793e | ||
|
|
9d942899ed | ||
|
|
8d3d91da2f | ||
|
|
fbadfc7144 | ||
|
|
e0b2316c42 | ||
|
|
8eb15ad97c | ||
|
|
136b240b99 | ||
|
|
4c67eaacd0 | ||
|
|
5aac86553c | ||
|
|
9fa9c9eb62 | ||
|
|
38b146155c | ||
|
|
2d559bac5d | ||
|
|
a1e99b4519 | ||
|
|
4f49685acd | ||
|
|
03c8188bf5 | ||
|
|
6bb363485d | ||
|
|
95a20b23c0 | ||
|
|
29a2429c00 | ||
|
|
4e86fc1656 | ||
|
|
9bdf7cdf1b | ||
|
|
5400c963a7 | ||
|
|
6ad0bab069 | ||
|
|
e916456d41 | ||
|
|
183da0053b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,5 +3,7 @@ node_modules/
|
|||||||
build/*
|
build/*
|
||||||
|
|
||||||
.~lock*
|
.~lock*
|
||||||
|
**/*(Copie)*
|
||||||
|
**/*(Copy)*
|
||||||
|
|
||||||
stats.html
|
stats.html
|
||||||
78
package-lock.json
generated
78
package-lock.json
generated
@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@odfjs/odfjs",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.18.0",
|
"version": "0.30.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@odfjs/odfjs",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.18.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": {
|
||||||
|
|||||||
11
package.json
11
package.json
@ -1,8 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@odfjs/odfjs",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.18.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
readme.md
19
readme.md
@ -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.18.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)**
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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);
|
||||||
|
|||||||
@ -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}'
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import {traverse, Node} from "../../DOMUtils.js";
|
//@ts-check
|
||||||
|
|
||||||
|
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
|
||||||
@ -35,41 +38,7 @@ function findAllMatches(text, pattern) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Node} node1
|
|
||||||
* @param {Node} node2
|
|
||||||
* @returns {Node | undefined}
|
|
||||||
*/
|
|
||||||
function findCommonAncestor(node1, node2) {
|
|
||||||
const ancestors1 = getAncestors(node1);
|
|
||||||
const ancestors2 = getAncestors(node2);
|
|
||||||
|
|
||||||
for(const ancestor of ancestors1) {
|
|
||||||
if(ancestors2.includes(ancestor)) {
|
|
||||||
return ancestor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @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
|
||||||
@ -98,19 +67,14 @@ function getNodeTextPosition(node, containerTextNodes) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* remove nodes between startNode and endNode
|
* remove nodes between startNode and endNode
|
||||||
* but keep startNode and endNode
|
* including startNode and endNode
|
||||||
*
|
|
||||||
* returns the common ancestor child in start branch
|
|
||||||
* for the purpose for inserting something between startNode and endNode
|
|
||||||
* with insertionPoint.parentNode.insertBefore(newBetweenContent, insertionPoint)
|
|
||||||
*
|
*
|
||||||
* @param {Node} startNode
|
* @param {Node} startNode
|
||||||
* @param {Node} endNode
|
* @param {Node} endNode
|
||||||
* @returns {Node}
|
* @param {string} text
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function removeNodesBetween(startNode, endNode) {
|
function replaceBetweenNodesWithText(startNode, endNode, text) {
|
||||||
let nodesToRemove = new Set();
|
|
||||||
|
|
||||||
// find both ancestry branch
|
// find both ancestry branch
|
||||||
const startNodeAncestors = new Set(getAncestors(startNode))
|
const startNodeAncestors = new Set(getAncestors(startNode))
|
||||||
const endNodeAncestors = new Set(getAncestors(endNode))
|
const endNodeAncestors = new Set(getAncestors(endNode))
|
||||||
@ -118,43 +82,42 @@ function removeNodesBetween(startNode, endNode) {
|
|||||||
// find common ancestor
|
// find common ancestor
|
||||||
const commonAncestor = findCommonAncestor(startNode, endNode)
|
const commonAncestor = findCommonAncestor(startNode, endNode)
|
||||||
|
|
||||||
// remove everything "on the right" of start branch
|
let remove = false
|
||||||
let currentAncestor = startNode
|
let toRemove = []
|
||||||
let commonAncestorChildInEndNodeBranch
|
let commonAncestorChild = commonAncestor.firstChild
|
||||||
|
let commonAncestorInsertionChild
|
||||||
|
|
||||||
while(currentAncestor !== commonAncestor){
|
while(commonAncestorChild){
|
||||||
let siblingToRemove = currentAncestor.nextSibling
|
if(startNodeAncestors.has(commonAncestorChild)){
|
||||||
|
remove = true
|
||||||
while(siblingToRemove && !endNodeAncestors.has(siblingToRemove)){
|
|
||||||
nodesToRemove.add(siblingToRemove)
|
|
||||||
siblingToRemove = siblingToRemove.nextSibling
|
|
||||||
}
|
|
||||||
if(endNodeAncestors.has(siblingToRemove)){
|
|
||||||
commonAncestorChildInEndNodeBranch = siblingToRemove
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAncestor = currentAncestor.parentNode;
|
if(remove){
|
||||||
}
|
toRemove.push(commonAncestorChild)
|
||||||
|
|
||||||
// remove everything "on the left" of end branch
|
if(endNodeAncestors.has(commonAncestorChild)){
|
||||||
currentAncestor = endNode
|
commonAncestorInsertionChild = commonAncestorChild.nextSibling
|
||||||
|
break;
|
||||||
while(currentAncestor !== commonAncestor){
|
}
|
||||||
let siblingToRemove = currentAncestor.previousSibling
|
|
||||||
|
|
||||||
while(siblingToRemove && !startNodeAncestors.has(siblingToRemove)){
|
|
||||||
nodesToRemove.add(siblingToRemove)
|
|
||||||
siblingToRemove = siblingToRemove.previousSibling
|
|
||||||
}
|
}
|
||||||
|
commonAncestorChild = commonAncestorChild.nextSibling
|
||||||
currentAncestor = currentAncestor.parentNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const node of nodesToRemove){
|
for(const node of toRemove){
|
||||||
node.parentNode.removeChild(node)
|
commonAncestor.removeChild(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('replaceBetweenNodesWithText startNode', startNode.textContent)
|
||||||
|
|
||||||
|
const newTextNode = commonAncestor.ownerDocument.createTextNode(text)
|
||||||
|
|
||||||
|
if(commonAncestorInsertionChild){
|
||||||
|
commonAncestor.insertBefore(newTextNode, commonAncestorInsertionChild)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
commonAncestor.appendChild(newTextNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return commonAncestorChildInEndNodeBranch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,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[]} */
|
||||||
@ -202,8 +166,10 @@ function consolidateMarkers(document){
|
|||||||
...findAllMatches(fullText, variableRegex)
|
...findAllMatches(fullText, variableRegex)
|
||||||
];
|
];
|
||||||
|
|
||||||
/*if(positionedMarkers.length >= 1)
|
|
||||||
console.log('positionedMarkers', positionedMarkers)*/
|
//if(positionedMarkers.length >= 1)
|
||||||
|
// console.log('positionedMarkers', positionedMarkers)
|
||||||
|
|
||||||
|
|
||||||
while(consolidatedMarkers.length < positionedMarkers.length) {
|
while(consolidatedMarkers.length < positionedMarkers.length) {
|
||||||
refreshContainerTextNodes()
|
refreshContainerTextNodes()
|
||||||
@ -246,6 +212,21 @@ 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)
|
||||||
|
|
||||||
|
/** @type {Node} */
|
||||||
|
let commonAncestorStartChild = startNode
|
||||||
|
while(commonAncestorStartChild.parentNode !== commonAncestor){
|
||||||
|
commonAncestorStartChild = commonAncestorStartChild.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Node} */
|
||||||
|
let commonAncestorEndChild = endNode
|
||||||
|
while(commonAncestorEndChild.parentNode !== commonAncestor){
|
||||||
|
commonAncestorEndChild = commonAncestorEndChild.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate relative positions within the nodes
|
// Calculate relative positions within the nodes
|
||||||
let startNodeTextContent = startNode.textContent || '';
|
let startNodeTextContent = startNode.textContent || '';
|
||||||
let endNodeTextContent = endNode.textContent || '';
|
let endNodeTextContent = endNode.textContent || '';
|
||||||
@ -256,47 +237,51 @@ function consolidateMarkers(document){
|
|||||||
// Calculate the position within the end node
|
// Calculate the position within the end node
|
||||||
let posInEndNode = (positionedMarker.index + positionedMarker.marker.length) - getNodeTextPosition(endNode, containerTextNodesInTreeOrder);
|
let posInEndNode = (positionedMarker.index + positionedMarker.marker.length) - getNodeTextPosition(endNode, containerTextNodesInTreeOrder);
|
||||||
|
|
||||||
/** @type {Node} */
|
let newStartNode = startNode
|
||||||
let beforeStartNode = startNode
|
|
||||||
|
|
||||||
// if there is before-text, split
|
// if there is before-text, split
|
||||||
if(posInStartNode > 0) {
|
if(posInStartNode > 0) {
|
||||||
// Text exists before the marker - preserve it
|
// Text exists before the marker - preserve it
|
||||||
|
|
||||||
// set newStartNode to a Text node containing only the marker beginning
|
// set newStartNode to a Text node containing only the marker beginning
|
||||||
const newStartNode = startNode.splitText(posInStartNode)
|
newStartNode = startNode.splitText(posInStartNode)
|
||||||
// startNode/beforeStartNode now contains only non-marker text
|
// startNode/beforeStartNode now contains only non-marker text
|
||||||
|
|
||||||
// then, by definition of .splitText(posInStartNode):
|
// then, by definition of .splitText(posInStartNode):
|
||||||
posInStartNode = 0
|
posInStartNode = 0
|
||||||
|
|
||||||
// remove the marker beginning part from the tree (since the marker will be inserted in full later)
|
// move the marker beginning part to become a child of commonAncestor
|
||||||
newStartNode.parentNode?.removeChild(newStartNode)
|
newStartNode.parentNode?.removeChild(newStartNode)
|
||||||
|
|
||||||
|
commonAncestor.insertBefore(newStartNode, commonAncestorStartChild.nextSibling)
|
||||||
|
|
||||||
|
//console.log('commonAncestor after before-text split', commonAncestor.textContent )
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Node} */
|
|
||||||
let afterEndNode
|
|
||||||
|
|
||||||
// if there is after-text, split
|
// if there is after-text, split
|
||||||
if(posInEndNode < endNodeTextContent.length) {
|
if(posInEndNode < endNodeTextContent.length) {
|
||||||
// Text exists after the marker - preserve it
|
// Text exists after the marker - preserve it
|
||||||
|
|
||||||
// set afterEndNode to a Text node containing only non-marker text
|
endNode.splitText(posInEndNode);
|
||||||
afterEndNode = endNode.splitText(posInEndNode);
|
|
||||||
// endNode now contains only the end of marker text
|
// endNode now contains only the end of marker text
|
||||||
|
|
||||||
// then, by definition of .splitText(posInEndNode):
|
// then, by definition of .splitText(posInEndNode):
|
||||||
posInEndNode = endNodeTextContent.length
|
posInEndNode = endNodeTextContent.length
|
||||||
|
|
||||||
// remove the marker ending part from the tree (since the marker will be inserted in full later)
|
// move the marker ending part to become a child of commonAncestor
|
||||||
endNode.parentNode?.removeChild(endNode)
|
if(endNode !== commonAncestorEndChild){
|
||||||
|
endNode.parentNode?.removeChild(endNode)
|
||||||
|
commonAncestor.insertBefore(endNode, commonAncestorEndChild)
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('commonAncestor after after-text split', commonAncestor.textContent )
|
||||||
}
|
}
|
||||||
|
|
||||||
// then, replace all nodes between (new)startNode and (new)endNode with a single textNode in commonAncestor
|
// then, replace all nodes between (new)startNode and (new)endNode with a single textNode in commonAncestor
|
||||||
const insertionPoint = removeNodesBetween(beforeStartNode, afterEndNode)
|
replaceBetweenNodesWithText(newStartNode, endNode, positionedMarker.marker)
|
||||||
const markerTextNode = insertionPoint.ownerDocument.createTextNode(positionedMarker.marker)
|
|
||||||
|
|
||||||
insertionPoint.parentNode.insertBefore(markerTextNode, insertionPoint)
|
//console.log('commonAncestor after replaceBetweenNodesWithText', commonAncestor.textContent )
|
||||||
|
|
||||||
// After consolidation, break as the DOM structure has changed
|
// After consolidation, break as the DOM structure has changed
|
||||||
// and containerTextNodesInTreeOrder needs to be refreshed
|
// and containerTextNodesInTreeOrder needs to be refreshed
|
||||||
@ -307,16 +292,35 @@ function consolidateMarkers(document){
|
|||||||
consolidatedMarkers.push(positionedMarker)
|
consolidatedMarkers.push(positionedMarker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//console.log('consolidatedMarkers', consolidatedMarkers)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {typeof closingIfMarker | typeof eachClosingMarker | typeof eachStartMarkerRegex.source | typeof elseMarker | typeof ifStartMarkerRegex.source | typeof variableRegex.source} MarkerType
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MarkerNode
|
||||||
|
* @prop {Node} node
|
||||||
|
* @prop {MarkerType} markerType
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isolate markers which are in Text nodes with other texts
|
* isolate markers which are in Text nodes with other texts
|
||||||
*
|
*
|
||||||
* @param {Document} document
|
* @param {Document} document
|
||||||
|
* @returns {Map<Node, MarkerType>}
|
||||||
*/
|
*/
|
||||||
function isolateMarkers(document){
|
function isolateMarkerText(document){
|
||||||
|
/** @type {ReturnType<isolateMarkerText>} */
|
||||||
|
const markerNodes = new Map()
|
||||||
|
|
||||||
traverse(document, currentNode => {
|
traverse(document, currentNode => {
|
||||||
|
//console.log('isolateMarkers', currentNode.nodeName, currentNode.textContent)
|
||||||
|
|
||||||
if(currentNode.nodeType === Node.TEXT_NODE) {
|
if(currentNode.nodeType === Node.TEXT_NODE) {
|
||||||
// find all marker starts and ends and split textNode
|
// find all marker starts and ends and split textNode
|
||||||
let remainingText = currentNode.textContent || ''
|
let remainingText = currentNode.textContent || ''
|
||||||
@ -324,6 +328,8 @@ function isolateMarkers(document){
|
|||||||
while(remainingText.length >= 1) {
|
while(remainingText.length >= 1) {
|
||||||
let matchText;
|
let matchText;
|
||||||
let matchIndex;
|
let matchIndex;
|
||||||
|
/** @type {MarkerType} */
|
||||||
|
let markerType;
|
||||||
|
|
||||||
// looking for a block marker
|
// looking for a block marker
|
||||||
for(const marker of [ifStartMarkerRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingMarker]) {
|
for(const marker of [ifStartMarkerRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingMarker]) {
|
||||||
@ -333,6 +339,7 @@ function isolateMarkers(document){
|
|||||||
if(index !== -1) {
|
if(index !== -1) {
|
||||||
matchText = marker
|
matchText = marker
|
||||||
matchIndex = index
|
matchIndex = index
|
||||||
|
markerType = marker
|
||||||
|
|
||||||
// found the first match
|
// found the first match
|
||||||
break; // get out of loop
|
break; // get out of loop
|
||||||
@ -345,6 +352,7 @@ function isolateMarkers(document){
|
|||||||
if(match) {
|
if(match) {
|
||||||
matchText = match[0]
|
matchText = match[0]
|
||||||
matchIndex = match.index
|
matchIndex = match.index
|
||||||
|
markerType = marker.source
|
||||||
|
|
||||||
// found the first match
|
// found the first match
|
||||||
break; // get out of loop
|
break; // get out of loop
|
||||||
@ -367,11 +375,21 @@ function isolateMarkers(document){
|
|||||||
|
|
||||||
// per spec, currentNode now contains before-match and match text
|
// per spec, currentNode now contains before-match and match text
|
||||||
|
|
||||||
|
/** @type {Node} */
|
||||||
|
let matchTextNode
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(matchIndex > 0) {
|
if(matchIndex > 0) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
currentNode.splitText(matchIndex)
|
matchTextNode = currentNode.splitText(matchIndex)
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
matchTextNode = currentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
markerNodes.set(matchTextNode, markerType)
|
||||||
|
|
||||||
|
// per spec, currentNode now contains only before-match text
|
||||||
|
|
||||||
if(afterMatchTextNode) {
|
if(afterMatchTextNode) {
|
||||||
currentNode = afterMatchTextNode
|
currentNode = afterMatchTextNode
|
||||||
@ -391,8 +409,102 @@ function isolateMarkers(document){
|
|||||||
// skip
|
// skip
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//console.log('markerNodes', [...markerNodes].map(([node, markerType]) => [node.textContent, markerType]))
|
||||||
|
|
||||||
|
return markerNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* after isolateMatchingMarkersStructure, matching markers (opening/closing each, if/then/closing if)
|
||||||
|
* are put in isolated branches within their common ancestors
|
||||||
|
*
|
||||||
|
* UNFINISHED - maybe another day if relevant
|
||||||
|
*
|
||||||
|
* @param {Document} document
|
||||||
|
* @param {Map<Node, MarkerType>} markerNodes
|
||||||
|
*/
|
||||||
|
//function isolateMatchingMarkersStructure(document, markerNodes){
|
||||||
|
/** @type {MarkerNode[]} */
|
||||||
|
/* let currentlyOpenBlocks = []
|
||||||
|
|
||||||
|
traverse(document, currentNode => {
|
||||||
|
|
||||||
|
const markerType = markerNodes.get(currentNode)
|
||||||
|
|
||||||
|
if(markerType){
|
||||||
|
switch(markerType){
|
||||||
|
case eachStartMarkerRegex.source:
|
||||||
|
case ifStartMarkerRegex.source: {
|
||||||
|
currentlyOpenBlocks.push({
|
||||||
|
node: currentNode,
|
||||||
|
markerType
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case eachClosingMarker: {
|
||||||
|
const lastOpenedBlockMarkerNode = currentlyOpenBlocks.pop()
|
||||||
|
|
||||||
|
if(!lastOpenedBlockMarkerNode)
|
||||||
|
throw new Error(`{/each} found without corresponding opening {#each x as y}`)
|
||||||
|
|
||||||
|
if(lastOpenedBlockMarkerNode.markerType !== eachStartMarkerRegex.source)
|
||||||
|
throw new Error(`{/each} found while the last opened block was not an opening {#each x as y} (it was a ${lastOpenedBlockMarkerNode.markerType})`)
|
||||||
|
|
||||||
|
const openingEachNode = lastOpenedBlockMarkerNode.node
|
||||||
|
const closingEachNode = currentNode
|
||||||
|
|
||||||
|
const commonAncestor = findCommonAncestor(openingEachNode, closingEachNode)
|
||||||
|
|
||||||
|
if(openingEachNode.parentNode !== commonAncestor && openingEachNode.parentNode.childNodes.length >= 2){
|
||||||
|
if(openingEachNode.previousSibling){
|
||||||
|
// create branch for previousSiblings
|
||||||
|
let previousSibling = openingEachNode.previousSibling
|
||||||
|
const previousSiblings = []
|
||||||
|
while(previousSibling){
|
||||||
|
previousSiblings.push(previousSibling.previousSibling)
|
||||||
|
previousSibling = previousSibling.previousSibling
|
||||||
|
}
|
||||||
|
|
||||||
|
// put previous siblings in tree order
|
||||||
|
previousSiblings.reverse()
|
||||||
|
|
||||||
|
const parent = openingEachNode.parentNode
|
||||||
|
const parentClone = parent.cloneNode(false)
|
||||||
|
for(const previousSibling of previousSiblings){
|
||||||
|
previousSibling.parentNode.removeChild(previousSibling)
|
||||||
|
parentClone.appendChild(previousSibling)
|
||||||
|
}
|
||||||
|
|
||||||
|
let openingEachNodeBranch = openingEachNode.parentNode
|
||||||
|
let branchForPreviousSiblings = parentClone
|
||||||
|
|
||||||
|
while(openingEachNodeBranch.parentNode !== commonAncestor){
|
||||||
|
const newParentClone = openingEachNodeBranch.parentNode.cloneNode(false)
|
||||||
|
branchForPreviousSiblings.parentNode.removeChild(branchForPreviousSiblings)
|
||||||
|
newParentClone.appendChild(branchForPreviousSiblings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new TypeError(`MarkerType not recognized: '${markerType}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function prepares the template DOM tree in a way that makes it easily processed by the template execution
|
* This function prepares the template DOM tree in a way that makes it easily processed by the template execution
|
||||||
* Specifically, after the call to this function, the document is altered to respect the following property:
|
* Specifically, after the call to this function, the document is altered to respect the following property:
|
||||||
@ -409,5 +521,10 @@ function isolateMarkers(document){
|
|||||||
*/
|
*/
|
||||||
export default function prepareTemplateDOMTree(document){
|
export default function prepareTemplateDOMTree(document){
|
||||||
consolidateMarkers(document)
|
consolidateMarkers(document)
|
||||||
isolateMarkers(document)
|
// after consolidateMarkers, each marker is in at most one text node
|
||||||
|
// (formatting with markers is removed)
|
||||||
|
|
||||||
|
isolateMarkerText(document)
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,4 +10,12 @@
|
|||||||
|
|
||||||
/** @typedef {string} SheetName */
|
/** @typedef {string} SheetName */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef OdfjsImage
|
||||||
|
* @prop {ArrayBuffer} content
|
||||||
|
* @prop {string} fileName
|
||||||
|
* @prop {string} mediaType
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
25
tests/fill-odt-template/complex.js
Normal file
25
tests/fill-odt-template/complex.js
Normal 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,`)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
@ -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}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -247,6 +248,43 @@ Hiver
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('template filling with text after {/each} in same text node', async t => {
|
||||||
|
const templatePath = join(import.meta.dirname, '../fixtures/text-after-closing-each.odt')
|
||||||
|
const templateContent = `Légumes de saison
|
||||||
|
|
||||||
|
{#each légumes as légume}
|
||||||
|
{légume},
|
||||||
|
{/each} en {saison}
|
||||||
|
`
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
saison: 'Printemps',
|
||||||
|
légumes: [
|
||||||
|
'Asperge',
|
||||||
|
'Betterave',
|
||||||
|
'Blette'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const odtTemplate = await getOdtTemplate(templatePath)
|
||||||
|
|
||||||
|
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||||
|
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||||
|
|
||||||
|
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||||
|
|
||||||
|
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||||
|
t.deepEqual(odtResultTextContent, `Légumes de saison
|
||||||
|
|
||||||
|
Asperge,
|
||||||
|
Betterave,
|
||||||
|
Blette,
|
||||||
|
en Printemps
|
||||||
|
`)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('template filling of a table', async t => {
|
test('template filling of a table', async t => {
|
||||||
const templatePath = join(import.meta.dirname, '../fixtures/tableau-simple.odt')
|
const templatePath = join(import.meta.dirname, '../fixtures/tableau-simple.odt')
|
||||||
const templateContent = `Évolution énergie en kWh par personne en France
|
const templateContent = `Évolution énergie en kWh par personne en France
|
||||||
@ -306,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, ``)
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
@ -5,6 +5,7 @@ import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js'
|
|||||||
|
|
||||||
import {fillOdtTemplate, getOdtTextContent} from '../../exports.js'
|
import {fillOdtTemplate, getOdtTextContent} from '../../exports.js'
|
||||||
|
|
||||||
|
|
||||||
test('template filling with several layers of formatting in {#each ...} start marker', async t => {
|
test('template filling with several layers of formatting in {#each ...} start marker', async t => {
|
||||||
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-plusieurs-couches.odt')
|
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-plusieurs-couches.odt')
|
||||||
const templateContent = `Liste de nombres
|
const templateContent = `Liste de nombres
|
||||||
@ -86,7 +87,6 @@ Les nombres : 3 5 8 13 !!
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test('template filling - {/each} and text after partially formatted', async t => {
|
test('template filling - {/each} and text after partially formatted', async t => {
|
||||||
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-each-end-and-after-formatted.odt')
|
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-each-end-and-after-formatted.odt')
|
||||||
const templateContent = `Liste de nombres
|
const templateContent = `Liste de nombres
|
||||||
@ -114,8 +114,6 @@ Les nombres : 5 8 13 21 !!
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test('template filling - partially formatted variable', async t => {
|
test('template filling - partially formatted variable', async t => {
|
||||||
const templatePath = join(import.meta.dirname, '../fixtures/partially-formatted-variable.odt')
|
const templatePath = join(import.meta.dirname, '../fixtures/partially-formatted-variable.odt')
|
||||||
const templateContent = `Nombre
|
const templateContent = `Nombre
|
||||||
@ -129,9 +127,9 @@ Voici le nombre : {nombre} !!!
|
|||||||
|
|
||||||
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)
|
||||||
|
//}catch(e){console.error(e)}
|
||||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||||
t.deepEqual(odtResultTextContent, `Nombre
|
t.deepEqual(odtResultTextContent, `Nombre
|
||||||
|
|
||||||
@ -139,3 +137,75 @@ Voici le nombre : 37 !!!
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('template filling - formatted-start-each-single-paragraph', async t => {
|
||||||
|
const templatePath = join(import.meta.dirname, '../fixtures/formatted-start-each-single-paragraph.odt')
|
||||||
|
const templateContent = `
|
||||||
|
{#each nombres as n}
|
||||||
|
{n}
|
||||||
|
{/each}
|
||||||
|
`
|
||||||
|
|
||||||
|
const data = {nombres : [37, 38, 39]}
|
||||||
|
|
||||||
|
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, `
|
||||||
|
|
||||||
|
37
|
||||||
|
38
|
||||||
|
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())
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
});
|
||||||
@ -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 d’utilisation 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`)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
BIN
tests/fixtures/basic-image-insertion.odt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/cellule avec style.ods
vendored
Normal file
BIN
tests/fixtures/cellule avec style.ods
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/cellules avec emails.ods
vendored
Normal file
BIN
tests/fixtures/cellules avec emails.ods
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/formatted-start-each-single-paragraph.odt
vendored
Normal file
BIN
tests/fixtures/formatted-start-each-single-paragraph.odt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ghost-if.odt
vendored
Normal file
BIN
tests/fixtures/ghost-if.odt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/if-then-each.odt
vendored
Normal file
BIN
tests/fixtures/if-then-each.odt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/left-branch-content-and-two-consecutive-ifs.odt
vendored
Normal file
BIN
tests/fixtures/left-branch-content-and-two-consecutive-ifs.odt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/nested-each-without-common-ancestor-for-inner-each.odt
vendored
Normal file
BIN
tests/fixtures/nested-each-without-common-ancestor-for-inner-each.odt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/pitchou-1.png
vendored
Normal file
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
BIN
tests/fixtures/pitchou-2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 799 KiB |
BIN
tests/fixtures/text-after-closing-each.odt
vendored
Normal file
BIN
tests/fixtures/text-after-closing-each.odt
vendored
Normal file
Binary file not shown.
@ -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');
|
||||||
|
});
|
||||||
|
|||||||
@ -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 '../scripts/node.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 : [
|
||||||
{
|
{
|
||||||
@ -79,8 +76,8 @@ 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},
|
||||||
@ -91,16 +88,60 @@ const data = {
|
|||||||
{ année: 2020, conso: 37859.246},
|
{ année: 2020, conso: 37859.246},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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 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)
|
||||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user