Compare commits

..

No commits in common. "main" and "v0.22.0" have entirely different histories.

27 changed files with 541 additions and 1253 deletions

78
package-lock.json generated
View File

@ -1,17 +1,16 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.30.0", "version": "0.22.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.30.0", "version": "0.22.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": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.30.0", "version": "0.22.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"
} }
} }

View File

@ -18,7 +18,7 @@ Small lib to parse/understand .odf files (.odt, .ods) in the browser and node.js
### Install ### Install
```sh ```sh
npm i https://github.com/odfjs/odfjs.git#v0.30.0 npm i https://github.com/odfjs/odfjs.git#v0.22.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)**

View File

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

View File

@ -10,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);
} }
} }

View File

@ -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();

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -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}'

View File

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

View File

@ -4,7 +4,7 @@ import { Uint8ArrayReader, ZipReader, TextWriter } from '@zip.js/zip.js';
import {parseXML} from './DOMUtils.js' import {parseXML} from './DOMUtils.js'
/** @import {Entry} from '@zip.js/zip.js'*/ /** @import {Entry} from '@zip.js/zip.js'*/
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent, 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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.')
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 KiB

View File

@ -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');
});

View File

@ -1,28 +1,31 @@
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'
import {fillOdtTemplate} from '../exports.js' import {fillOdtTemplate} from '../exports.js'
/* /*
const templatePath = join(import.meta.dirname, '../tests/fixtures/template-anniversaire.odt') const templatePath = join(import.meta.dirname, '../tests/data/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/fixtures/liste-fruits-et-légumes.odt') const templatePath = join(import.meta.dirname, '../tests/data/liste-fruits-et-légumes.odt')
const data = { const data = {
fruits : [ fruits : [
'Pastèque 🍉', 'Pastèque 🍉',
@ -37,7 +40,7 @@ const data = {
}*/ }*/
/* /*
const templatePath = join(import.meta.dirname, '../tests/fixtures/légumes-de-saison.odt') const templatePath = join(import.meta.dirname, '../tests/data/légumes-de-saison.odt')
const data = { const data = {
légumesSaison : [ légumesSaison : [
{ {
@ -77,7 +80,7 @@ const data = {
*/ */
/* /*
const templatePath = join(import.meta.dirname, '../tests/fixtures/tableau-simple.odt') const templatePath = join(import.meta.dirname, '../tests/data/tableau-simple.odt')
const data = { const data = {
annéeConsos : [ annéeConsos : [
{ année: 1970, conso: 36252.637}, { année: 1970, conso: 36252.637},
@ -92,55 +95,17 @@ const data = {
/* /*
const templatePath = join(import.meta.dirname, '../tests/fixtures/template-avec-image.odt') const templatePath = join(import.meta.dirname, '../tests/data/template-avec-image.odt')
const data = { const data = {
commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶` commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶`
} }
*/ */
/*
const templatePath = join(import.meta.dirname, '../tests/fixtures/partially-formatted-variable.odt') const templatePath = join(import.meta.dirname, '../tests/fixtures/partially-formatted-variable.odt')
const data = {nombre : 37} const data = {nombre : 37}
*/
/*
const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
const data = {
saison: 'Printemps',
légumes: [
'Asperge',
'Betterave',
'Blette'
]
}
*/
// const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
// const data = {
// saison: 'Printemps',
// légumes: [
// 'Asperge',
// 'Betterave',
// 'Blette'
// ]
// }
// const templatePath = join(import.meta.dirname, '../tests/fixtures/if-then-each.odt')
// const data = {liste_départements : ['95', '33']}
const templatePath = join(import.meta.dirname, '../tests/fixtures/basic-image-insertion.odt')
const photo1Path = join(import.meta.dirname, '../tests/fixtures/pitchou-1.png')
const photo2Path = join(import.meta.dirname, '../tests/fixtures/pitchou-2.png')
const photo1Buffer = (await readFile(photo1Path)).buffer
const photo2Buffer = (await readFile(photo2Path)).buffer
const photos = [{content: photo1Buffer, fileName: 'pitchou-1.png', mediaType: 'image/png'}, {content: photo2Buffer, fileName: 'pitchou-2.png', mediaType: 'image/png'}]
const data = {
title: 'Titre de mon projet',
photos,
}
const odtTemplate = await getOdtTemplate(templatePath) const odtTemplate = await getOdtTemplate(templatePath)