Compare commits
No commits in common. "main" and "v0.24.0" have entirely different histories.
78
package-lock.json
generated
78
package-lock.json
generated
@ -1,17 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@odfjs/odfjs",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.30.0",
|
"version": "0.24.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@odfjs/odfjs",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.30.0",
|
"version": "0.24.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",
|
||||||
"image-size": "^2.0.2",
|
"ses": "^1.12.0"
|
||||||
"ses": "^1.14.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
@ -42,22 +41,10 @@
|
|||||||
"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.11",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz",
|
||||||
"integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==",
|
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==",
|
||||||
"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": {
|
||||||
@ -2198,18 +2185,6 @@
|
|||||||
"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",
|
||||||
@ -3640,14 +3615,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ses": {
|
"node_modules/ses": {
|
||||||
"version": "1.14.0",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz",
|
||||||
"integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
|
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@endo/cache-map": "^1.1.0",
|
"@endo/env-options": "^1.1.8"
|
||||||
"@endo/env-options": "^1.1.11",
|
|
||||||
"@endo/immutable-arraybuffer": "^1.1.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-blocking": {
|
"node_modules/set-blocking": {
|
||||||
@ -4598,20 +4571,10 @@
|
|||||||
"@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.11",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz",
|
||||||
"integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA=="
|
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg=="
|
||||||
},
|
|
||||||
"@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",
|
||||||
@ -6166,11 +6129,6 @@
|
|||||||
"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",
|
||||||
@ -7180,13 +7138,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ses": {
|
"ses": {
|
||||||
"version": "1.14.0",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz",
|
||||||
"integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
|
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@endo/cache-map": "^1.1.0",
|
"@endo/env-options": "^1.1.8"
|
||||||
"@endo/env-options": "^1.1.11",
|
|
||||||
"@endo/immutable-arraybuffer": "^1.1.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set-blocking": {
|
"set-blocking": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odfjs/odfjs",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.30.0",
|
"version": "0.24.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://source.netsyms.com/PostalPortal/odfjs.git"
|
"url": "https://github.com/odfjs/odfjs.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
@ -41,7 +41,6 @@
|
|||||||
"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",
|
||||||
"image-size": "^2.0.2",
|
"ses": "^1.12.0"
|
||||||
"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.30.0
|
npm i https://github.com/odfjs/odfjs.git#v0.24.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -99,7 +99,8 @@ And then run the code:
|
|||||||
```js
|
```js
|
||||||
import {join} from 'node:path';
|
import {join} from 'node:path';
|
||||||
|
|
||||||
import {getOdtTemplate, fillOdtTemplate} from '@odfjs/odfjs'
|
import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js'
|
||||||
|
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')
|
||||||
@ -125,19 +126,6 @@ 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/
|
||||||
@ -158,3 +146,4 @@ 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)**
|
||||||
|
|
||||||
|
|||||||
@ -10,11 +10,7 @@ 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>`;
|
||||||
@ -31,7 +27,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, currencyData = null) {
|
export async function createOdsFile(sheetsData) {
|
||||||
// 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'));
|
||||||
|
|
||||||
@ -49,7 +45,7 @@ export async function createOdsFile(sheetsData, currencyData = null) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const contentXml = generateContentFileXMLString(sheetsData, currencyData);
|
const contentXml = generateContentFileXMLString(sheetsData);
|
||||||
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));
|
||||||
@ -67,7 +63,7 @@ export async function createOdsFile(sheetsData, currencyData = null) {
|
|||||||
* @param {Map<SheetName, SheetRawContent>} sheetsData
|
* @param {Map<SheetName, SheetRawContent>} sheetsData
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function generateContentFileXMLString(sheetsData, currencyData) {
|
function generateContentFileXMLString(sheetsData) {
|
||||||
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;
|
||||||
|
|
||||||
@ -79,52 +75,6 @@ function generateContentFileXMLString(sheetsData, currencyData) {
|
|||||||
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);
|
||||||
|
|
||||||
@ -137,30 +87,8 @@ function generateContentFileXMLString(sheetsData, currencyData) {
|
|||||||
tableNode.setAttribute('table:name', sheetName);
|
tableNode.setAttribute('table:name', sheetName);
|
||||||
spreadsheetNode.appendChild(tableNode);
|
spreadsheetNode.appendChild(tableNode);
|
||||||
|
|
||||||
var columnsWidthChars = {};
|
const columnNode = doc.createElement('table:table-column');
|
||||||
for (let r = 0; r < sheetData.length; r++) {
|
tableNode.appendChild(columnNode);
|
||||||
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) => {
|
||||||
@ -173,10 +101,6 @@ function generateContentFileXMLString(sheetsData, currencyData) {
|
|||||||
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) {
|
||||||
@ -187,14 +111,6 @@ function generateContentFileXMLString(sheetsData, currencyData) {
|
|||||||
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;
|
||||||
@ -210,11 +126,7 @@ function generateContentFileXMLString(sheetsData, currencyData) {
|
|||||||
|
|
||||||
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.value.toString();
|
||||||
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>}
|
||||||
*/
|
*/
|
||||||
export async function getContentDocument(odtFile) {
|
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();
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import {traverse, Node, getAncestors, findCommonAncestor} from "../../DOMUtils.js";
|
import {traverse, Node, getAncestors, findCommonAncestor} from "../../DOMUtils.js";
|
||||||
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, imageMarkerRegex, variableRegex} from './markers.js'
|
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js'
|
||||||
import {isOdfjsImage} from "../../shared.js"
|
|
||||||
import imageSize from "image-size";
|
|
||||||
/** @import {OdfjsImage} from "../../types.js" */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef TextPlaceToFill
|
* @typedef TextPlaceToFill
|
||||||
@ -17,7 +14,7 @@ class TemplateDOMBranch{
|
|||||||
/** @type {Node} */
|
/** @type {Node} */
|
||||||
#leafNode
|
#leafNode
|
||||||
|
|
||||||
// ancestors with this.#ancestors[0] === this.#branchBaseNode and this.#ancestors.at(-1) === this.#leafNode
|
// ancestors with this.#ancestors[0] === this.#startNode and this.#ancestors.at(-1) === this.#leafNode
|
||||||
/** @type {Node[]} */
|
/** @type {Node[]} */
|
||||||
#ancestors
|
#ancestors
|
||||||
|
|
||||||
@ -185,18 +182,12 @@ class TemplateBlock{
|
|||||||
/** @type {Node[]} */
|
/** @type {Node[]} */
|
||||||
#middleContent;
|
#middleContent;
|
||||||
|
|
||||||
/**@type {any} */
|
|
||||||
#addImageToOdtFile;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Node} startNode
|
* @param {Node} startNode
|
||||||
* @param {Node} endNode
|
* @param {Node} endNode
|
||||||
* @param {(OdfjsImage) => string} addImageToOdtFile
|
|
||||||
*/
|
*/
|
||||||
constructor(startNode, endNode, addImageToOdtFile){
|
constructor(startNode, endNode){
|
||||||
this.#addImageToOdtFile = addImageToOdtFile
|
|
||||||
|
|
||||||
// @ts-expect-error xmldom.Node
|
// @ts-expect-error xmldom.Node
|
||||||
this.#commonAncestor = findCommonAncestor(startNode, endNode)
|
this.#commonAncestor = findCommonAncestor(startNode, endNode)
|
||||||
|
|
||||||
@ -240,16 +231,13 @@ class TemplateBlock{
|
|||||||
const startChild = this.startBranch.at(1)
|
const startChild = this.startBranch.at(1)
|
||||||
if(startChild /*&& startChild !== */){
|
if(startChild /*&& startChild !== */){
|
||||||
//console.log('[fillBlockContentTemplate] startChild', startChild.nodeName, startChild.textContent)
|
//console.log('[fillBlockContentTemplate] startChild', startChild.nodeName, startChild.textContent)
|
||||||
fillOdtElementTemplate(startChild, compartement, this.#addImageToOdtFile)
|
fillOdtElementTemplate(startChild, compartement)
|
||||||
}
|
}
|
||||||
//console.log('[fillBlockContentTemplate] after startChild')
|
//console.log('[fillBlockContentTemplate] after startChild')
|
||||||
|
|
||||||
|
for(const content of this.#middleContent){
|
||||||
// if content consists of several parts of an {#each}{/each}
|
fillOdtElementTemplate(content, compartement)
|
||||||
// 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')
|
//console.log('[fillBlockContentTemplate] after middleContent')
|
||||||
|
|
||||||
const endChild = this.endBranch.at(1)
|
const endChild = this.endBranch.at(1)
|
||||||
@ -258,7 +246,7 @@ class TemplateBlock{
|
|||||||
|
|
||||||
if(endChild){
|
if(endChild){
|
||||||
//console.log('[fillBlockContentTemplate] endChild', endChild.nodeName, endChild.textContent)
|
//console.log('[fillBlockContentTemplate] endChild', endChild.nodeName, endChild.textContent)
|
||||||
fillOdtElementTemplate(endChild, compartement, this.#addImageToOdtFile)
|
fillOdtElementTemplate(endChild, compartement)
|
||||||
}
|
}
|
||||||
//console.log('[fillBlockContentTemplate] after endChild')
|
//console.log('[fillBlockContentTemplate] after endChild')
|
||||||
|
|
||||||
@ -356,7 +344,7 @@ class TemplateBlock{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TemplateBlock(startLeafCloneNode, endLeafCloneNode, this.#addImageToOdtFile)
|
return new TemplateBlock(startLeafCloneNode, endLeafCloneNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -435,9 +423,8 @@ function findPlacesToFillInString(str, compartment) {
|
|||||||
* @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, addImageToOdtFile) {
|
function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment) {
|
||||||
//const docEl = ifOpeningMarkerNode.ownerDocument.documentElement
|
//const docEl = ifOpeningMarkerNode.ownerDocument.documentElement
|
||||||
|
|
||||||
const conditionValue = compartment.evaluate(ifBlockConditionExpression)
|
const conditionValue = compartment.evaluate(ifBlockConditionExpression)
|
||||||
@ -453,11 +440,11 @@ function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode,
|
|||||||
ifElseMarkerNode.childNodes.length, ifElseMarkerNode.textContent
|
ifElseMarkerNode.childNodes.length, ifElseMarkerNode.textContent
|
||||||
)*/
|
)*/
|
||||||
|
|
||||||
thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifElseMarkerNode, addImageToOdtFile)
|
thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifElseMarkerNode)
|
||||||
elseTemplateBlock = new TemplateBlock(ifElseMarkerNode, ifClosingMarkerNode, addImageToOdtFile)
|
elseTemplateBlock = new TemplateBlock(ifElseMarkerNode, ifClosingMarkerNode)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifClosingMarkerNode, addImageToOdtFile)
|
thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifClosingMarkerNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(conditionValue) {
|
if(conditionValue) {
|
||||||
@ -496,15 +483,14 @@ 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, addImageToOdtFile) {
|
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment) {
|
||||||
//console.log('fillEachBlock', iterableExpression, itemExpression)
|
//console.log('fillEachBlock', iterableExpression, itemExpression)
|
||||||
|
|
||||||
const docEl = startNode.ownerDocument.documentElement
|
const docEl = startNode.ownerDocument.documentElement
|
||||||
//console.log('[fillEachBlock] docEl', docEl.textContent)
|
//console.log('[fillEachBlock] docEl', docEl.textContent)
|
||||||
|
|
||||||
const repeatedTemplateBlock = new TemplateBlock(startNode, endNode, addImageToOdtFile)
|
const repeatedTemplateBlock = new TemplateBlock(startNode, endNode)
|
||||||
|
|
||||||
// Find the iterable in the data
|
// Find the iterable in the data
|
||||||
let iterable = compartment.evaluate(iterableExpression)
|
let iterable = compartment.evaluate(iterableExpression)
|
||||||
@ -565,49 +551,17 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 {RootElementArgument | RootElementArgument[]} rootElements
|
* @param {Element | DocumentFragment | Document} rootElement
|
||||||
* @param {Compartment} compartment
|
* @param {Compartment} compartment
|
||||||
* @param {(OdfjsImage) => string} addImageToOdtFile
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export default function fillOdtElementTemplate(rootElements, compartment, addImageToOdtFile) {
|
export default function fillOdtElementTemplate(rootElement, compartment) {
|
||||||
|
//console.log('[fillTemplatedOdtElement]', rootElement.nodeType, rootElement.nodeName, rootElement.textContent)
|
||||||
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)
|
//console.log('[fillTemplatedOdtElement]', rootElement.documentElement && rootElement.documentElement.textContent)
|
||||||
|
|
||||||
let currentlyOpenBlocks = []
|
let currentlyOpenBlocks = []
|
||||||
@ -631,245 +585,187 @@ export default function fillOdtElementTemplate(rootElements, compartment, addIma
|
|||||||
// Traverse "in document order"
|
// Traverse "in document order"
|
||||||
|
|
||||||
|
|
||||||
for(const rootElement of rootElements){
|
// @ts-ignore
|
||||||
|
traverse(rootElement, currentNode => {
|
||||||
|
//console.log('currentlyOpenBlocks', currentlyOpenBlocks)
|
||||||
|
|
||||||
// @ts-ignore
|
const insideAnOpenBlock = currentlyOpenBlocks.length >= 1
|
||||||
traverse(rootElement, currentNode => {
|
|
||||||
//console.log('currentlyOpenBlocks', currentlyOpenBlocks)
|
|
||||||
//console.log('eachOpeningMarkerNode', eachOpeningMarkerNode)
|
|
||||||
|
|
||||||
const insideAnOpenBlock = currentlyOpenBlocks.length >= 1
|
if(currentNode.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = currentNode.textContent || ''
|
||||||
|
|
||||||
if(currentNode.nodeType === Node.TEXT_NODE) {
|
/**
|
||||||
const text = currentNode.textContent || ''
|
* looking for {#each x as y}
|
||||||
|
*/
|
||||||
|
const eachStartMatch = text.match(eachStartMarkerRegex);
|
||||||
|
|
||||||
/**
|
if(eachStartMatch) {
|
||||||
* looking for {#each x as y}
|
//console.log('startMatch', startMatch)
|
||||||
*/
|
|
||||||
const eachStartMatch = text.match(eachStartMarkerRegex);
|
|
||||||
|
|
||||||
if(eachStartMatch) {
|
currentlyOpenBlocks.push(EACH)
|
||||||
//console.log('startMatch', startMatch)
|
|
||||||
|
|
||||||
currentlyOpenBlocks.push(EACH)
|
if(insideAnOpenBlock) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let [_, _iterableExpression, _itemExpression] = eachStartMatch
|
||||||
|
|
||||||
if(insideAnOpenBlock) {
|
eachBlockIterableExpression = _iterableExpression
|
||||||
// do nothing
|
eachBlockItemExpression = _itemExpression
|
||||||
}
|
eachOpeningMarkerNode = currentNode
|
||||||
else {
|
}
|
||||||
let [_, _iterableExpression, _itemExpression] = eachStartMatch
|
}
|
||||||
|
|
||||||
eachBlockIterableExpression = _iterableExpression
|
|
||||||
eachBlockItemExpression = _itemExpression
|
/**
|
||||||
eachOpeningMarkerNode = currentNode
|
* Looking for {/each}
|
||||||
}
|
*/
|
||||||
|
const isEachClosingBlock = text.includes(eachClosingMarker)
|
||||||
|
|
||||||
|
if(isEachClosingBlock) {
|
||||||
|
|
||||||
|
//console.log('isEachClosingBlock', isEachClosingBlock)
|
||||||
|
|
||||||
|
if(!eachOpeningMarkerNode)
|
||||||
|
throw new Error(`{/each} found without corresponding opening {#each x as y}`)
|
||||||
|
|
||||||
|
if(currentlyOpenBlocks.at(-1) !== EACH)
|
||||||
|
throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`)
|
||||||
|
|
||||||
|
if(currentlyOpenBlocks.length === 1) {
|
||||||
|
eachClosingMarkerNode = currentNode
|
||||||
|
|
||||||
|
// found an {#each} and its corresponding {/each}
|
||||||
|
// execute replacement loop
|
||||||
|
fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment)
|
||||||
|
|
||||||
|
eachOpeningMarkerNode = undefined
|
||||||
|
eachBlockIterableExpression = undefined
|
||||||
|
eachBlockItemExpression = undefined
|
||||||
|
eachClosingMarkerNode = undefined
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// ignore because it will be treated as part of the outer {#each}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentlyOpenBlocks.pop()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Looking for {/each}
|
|
||||||
*/
|
|
||||||
const isEachClosingBlock = text.includes(eachClosingMarker)
|
|
||||||
|
|
||||||
if(isEachClosingBlock) {
|
/**
|
||||||
|
* Looking for {#if ...}
|
||||||
|
*/
|
||||||
|
const ifStartMatch = text.match(ifStartMarkerRegex);
|
||||||
|
|
||||||
//console.log('isEachClosingBlock', isEachClosingBlock, currentlyOpenBlocks)
|
if(ifStartMatch) {
|
||||||
|
currentlyOpenBlocks.push(IF)
|
||||||
|
|
||||||
if(!insideAnOpenBlock)
|
if(insideAnOpenBlock) {
|
||||||
throw new Error('{/each} found without corresponding opening {#each x as y}')
|
// do nothing because the marker is too deep
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let [_, _ifBlockConditionExpression] = ifStartMatch
|
||||||
|
|
||||||
if(currentlyOpenBlocks.at(-1) !== EACH)
|
ifBlockConditionExpression = _ifBlockConditionExpression
|
||||||
throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`)
|
ifOpeningMarkerNode = currentNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(currentlyOpenBlocks.length === 1) {
|
|
||||||
eachClosingMarkerNode = currentNode
|
|
||||||
|
|
||||||
// found an {#each} and its corresponding {/each}
|
/**
|
||||||
|
* Looking for {:else}
|
||||||
|
*/
|
||||||
|
const hasElseMarker = text.includes(elseMarker);
|
||||||
|
|
||||||
|
if(hasElseMarker) {
|
||||||
|
if(!insideAnOpenBlock)
|
||||||
|
throw new Error('{:else} without a corresponding {#if}')
|
||||||
|
|
||||||
|
if(currentlyOpenBlocks.length === 1) {
|
||||||
|
if(currentlyOpenBlocks[0] === IF) {
|
||||||
|
ifElseMarkerNode = currentNode
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new Error('{:else} inside an {#each} but without a corresponding {#if}')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// do nothing because the marker is too deep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looking for {/if}
|
||||||
|
*/
|
||||||
|
const ifClosingMarker = text.includes(closingIfMarker);
|
||||||
|
|
||||||
|
if(ifClosingMarker) {
|
||||||
|
if(!insideAnOpenBlock)
|
||||||
|
throw new Error('{/if} without a corresponding {#if}')
|
||||||
|
|
||||||
|
if(currentlyOpenBlocks.length === 1) {
|
||||||
|
if(currentlyOpenBlocks[0] === IF) {
|
||||||
|
ifClosingMarkerNode = currentNode
|
||||||
|
|
||||||
|
// found an {#if} and its corresponding {/if}
|
||||||
// execute replacement loop
|
// execute replacement loop
|
||||||
//console.log('start of fillEachBlock')
|
fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment)
|
||||||
|
|
||||||
fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment, addImageToOdtFile)
|
ifOpeningMarkerNode = undefined
|
||||||
|
ifElseMarkerNode = undefined
|
||||||
//console.log('end of fillEachBlock')
|
ifClosingMarkerNode = undefined
|
||||||
|
ifBlockConditionExpression = undefined
|
||||||
eachOpeningMarkerNode = undefined
|
|
||||||
eachBlockIterableExpression = undefined
|
|
||||||
eachBlockItemExpression = undefined
|
|
||||||
eachClosingMarkerNode = undefined
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// ignore because it will be treated as part of the outer {#each}
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log('popping currentlyOpenBlocks')
|
|
||||||
currentlyOpenBlocks.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looking for {#if ...}
|
|
||||||
*/
|
|
||||||
const ifStartMatch = text.match(ifStartMarkerRegex);
|
|
||||||
|
|
||||||
if(ifStartMatch) {
|
|
||||||
currentlyOpenBlocks.push(IF)
|
|
||||||
|
|
||||||
if(insideAnOpenBlock) {
|
|
||||||
// do nothing because the marker is too deep
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let [_, _ifBlockConditionExpression] = ifStartMatch
|
|
||||||
|
|
||||||
ifBlockConditionExpression = _ifBlockConditionExpression
|
|
||||||
ifOpeningMarkerNode = currentNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looking for {:else}
|
|
||||||
*/
|
|
||||||
const hasElseMarker = text.includes(elseMarker);
|
|
||||||
|
|
||||||
if(hasElseMarker) {
|
|
||||||
if(!insideAnOpenBlock)
|
|
||||||
throw new Error('{:else} without a corresponding {#if}')
|
|
||||||
|
|
||||||
if(currentlyOpenBlocks.length === 1) {
|
|
||||||
if(currentlyOpenBlocks[0] === IF) {
|
|
||||||
ifElseMarkerNode = currentNode
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new Error('{:else} inside an {#each} but without a corresponding {#if}')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// do nothing because the marker is too deep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looking for {/if}
|
|
||||||
*/
|
|
||||||
const ifClosingMarker = text.includes(closingIfMarker);
|
|
||||||
|
|
||||||
if(ifClosingMarker) {
|
|
||||||
if(!insideAnOpenBlock)
|
|
||||||
throw new Error('{/if} without a corresponding {#if}')
|
|
||||||
|
|
||||||
if(currentlyOpenBlocks.length === 1) {
|
|
||||||
if(currentlyOpenBlocks[0] === IF) {
|
|
||||||
ifClosingMarkerNode = currentNode
|
|
||||||
|
|
||||||
// found an {#if} and its corresponding {/if}
|
|
||||||
// execute replacement loop
|
|
||||||
fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment, addImageToOdtFile)
|
|
||||||
|
|
||||||
ifOpeningMarkerNode = undefined
|
|
||||||
ifElseMarkerNode = undefined
|
|
||||||
ifClosingMarkerNode = undefined
|
|
||||||
ifBlockConditionExpression = undefined
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new Error('{/if} inside an {#each} but without a corresponding {#if}')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// do nothing because the marker is too deep
|
|
||||||
}
|
|
||||||
|
|
||||||
currentlyOpenBlocks.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looking for variables for substitutions
|
|
||||||
*/
|
|
||||||
if(!insideAnOpenBlock) {
|
|
||||||
// @ts-ignore
|
|
||||||
if(currentNode.data) {
|
|
||||||
// @ts-ignore
|
|
||||||
const placesToFill = findPlacesToFillInString(currentNode.data, compartment)
|
|
||||||
|
|
||||||
if(placesToFill) {
|
|
||||||
const newText = placesToFill.fill()
|
|
||||||
// @ts-ignore
|
|
||||||
const newTextNode = currentNode.ownerDocument?.createTextNode(newText)
|
|
||||||
// @ts-ignore
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
throw new Error('{/if} inside an {#each} but without a corresponding {#if}')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// ignore because it will be treated as part of the outer {#each} block
|
// do nothing because the marker is too deep
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentlyOpenBlocks.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(currentNode.nodeType === Node.ATTRIBUTE_NODE) {
|
|
||||||
// Looking for variables for substitutions
|
/**
|
||||||
if(!insideAnOpenBlock) {
|
* Looking for variables for substitutions
|
||||||
|
*/
|
||||||
|
if(!insideAnOpenBlock) {
|
||||||
|
// @ts-ignore
|
||||||
|
if(currentNode.data) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(currentNode.value) {
|
const placesToFill = findPlacesToFillInString(currentNode.data, compartment)
|
||||||
|
|
||||||
|
if(placesToFill) {
|
||||||
|
const newText = placesToFill.fill()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const placesToFill = findPlacesToFillInString(currentNode.value, compartment)
|
const newTextNode = currentNode.ownerDocument?.createTextNode(newText)
|
||||||
if(placesToFill) {
|
// @ts-ignore
|
||||||
// @ts-ignore
|
currentNode.parentNode?.replaceChild(newTextNode, currentNode)
|
||||||
currentNode.value = placesToFill.fill()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
// ignore because it will be treated as part of the {#each} block
|
else {
|
||||||
|
// ignore because it will be treated as part of the outer {#each} block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(currentNode.nodeType === Node.ATTRIBUTE_NODE) {
|
||||||
|
// Looking for variables for substitutions
|
||||||
|
if(!insideAnOpenBlock) {
|
||||||
|
// @ts-ignore
|
||||||
|
if(currentNode.value) {
|
||||||
|
// @ts-ignore
|
||||||
|
const placesToFill = findPlacesToFillInString(currentNode.value, compartment)
|
||||||
|
if(placesToFill) {
|
||||||
|
// @ts-ignore
|
||||||
|
currentNode.value = placesToFill.fill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
else {
|
||||||
|
// ignore because it will be treated as part of the {#each} block
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,11 @@ 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, ODFManifestFileEntry} from '../manifest.js' */
|
/** @import {ODFManifest} from '../manifest.js' */
|
||||||
/** @import {OdfjsImage} from '../../types.js' */
|
|
||||||
|
|
||||||
/** @typedef {ArrayBuffer} ODTFile */
|
/** @typedef {ArrayBuffer} ODTFile */
|
||||||
|
|
||||||
@ -22,12 +23,11 @@ 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, addImageToOdtFile) {
|
function fillOdtDocumentTemplate(document, compartment) {
|
||||||
prepareTemplateDOMTree(document)
|
prepareTemplateDOMTree(document)
|
||||||
fillOdtElementTemplate(document, compartment, addImageToOdtFile)
|
fillOdtElementTemplate(document, compartment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -64,21 +64,6 @@ 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) {
|
||||||
@ -112,14 +97,12 @@ 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, addImageToOdtFile)
|
fillOdtDocumentTemplate(contentDocument, compartment)
|
||||||
|
|
||||||
const updatedContentXml = serializeToString(contentDocument)
|
const updatedContentXml = serializeToString(contentDocument)
|
||||||
|
|
||||||
@ -155,9 +138,6 @@ 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,6 +1,5 @@
|
|||||||
// 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}'
|
||||||
|
|||||||
@ -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, OdfjsImage} from './types.js' */
|
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent} from './types.js' */
|
||||||
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-node
|
// https://dom.spec.whatwg.org/#interface-node
|
||||||
@ -18,6 +18,7 @@ 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
|
||||||
@ -33,8 +34,6 @@ 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') {
|
||||||
@ -161,22 +160,6 @@ 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,12 +10,4 @@
|
|||||||
|
|
||||||
/** @typedef {string} SheetName */
|
/** @typedef {string} SheetName */
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef OdfjsImage
|
|
||||||
* @prop {ArrayBuffer} content
|
|
||||||
* @prop {string} fileName
|
|
||||||
* @prop {string} mediaType
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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,`)
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -344,32 +344,3 @@ 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, ``)
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
@ -1,12 +1,10 @@
|
|||||||
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, getOdtTextContent} from '../../exports.js'
|
import {fillOdtTemplate} 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 => {
|
||||||
@ -38,47 +36,3 @@ 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
BIN
tests/fixtures/basic-image-insertion.odt
vendored
Binary file not shown.
BIN
tests/fixtures/cellule avec style.ods
vendored
BIN
tests/fixtures/cellule avec style.ods
vendored
Binary file not shown.
BIN
tests/fixtures/cellules avec emails.ods
vendored
BIN
tests/fixtures/cellules avec emails.ods
vendored
Binary file not shown.
BIN
tests/fixtures/if-then-each.odt
vendored
BIN
tests/fixtures/if-then-each.odt
vendored
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/pitchou-1.png
vendored
BIN
tests/fixtures/pitchou-1.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 768 KiB |
BIN
tests/fixtures/pitchou-2.png
vendored
BIN
tests/fixtures/pitchou-2.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 799 KiB |
@ -56,33 +56,3 @@ 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,4 +1,4 @@
|
|||||||
import {writeFile, readFile} from 'node:fs/promises'
|
import {writeFile} 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'
|
||||||
@ -103,7 +103,6 @@ const templatePath = join(import.meta.dirname, '../tests/fixtures/partially-form
|
|||||||
const data = {nombre : 37}
|
const data = {nombre : 37}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
|
const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
|
||||||
const data = {
|
const data = {
|
||||||
saison: 'Printemps',
|
saison: 'Printemps',
|
||||||
@ -113,34 +112,7 @@ const data = {
|
|||||||
'Blette'
|
'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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user