Compare commits

...

14 Commits

Author SHA1 Message Date
e6e09a0361 Properly support currency fields
Some checks failed
Build and Deploy / build (push) Failing after 5m8s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 03:32:14 -07:00
3d98e41d2b Add column width calculation
Some checks failed
Build and Deploy / build (push) Failing after 11s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 01:54:56 -07:00
7c87d3220d Update repository url
Some checks failed
Build and Deploy / build (push) Failing after 12s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 01:10:55 -07:00
8b60ceb39c Add bold cell formatting option
Some checks failed
Build and Deploy / build (push) Failing after 14s
Build and Deploy / deploy (push) Has been skipped
2026-02-05 01:10:10 -07:00
Hannaeko
c677c7267b bump readme version 2025-09-22 16:35:43 +02:00
Hannaeko
415a26e3f1 0.30.0 2025-09-22 16:35:13 +02:00
hannaeko
90b97e23e9
fix text extraction for cells with partial styling (#23) 2025-09-22 16:34:43 +02:00
David Bruant
02d5338634 bump readme version 2025-09-18 16:16:21 +02:00
David Bruant
542183a593 0.29.0 2025-09-18 16:15:55 +02:00
David Bruant
a31f57026d
Remove default call to lockdown (#22)
* restore test

* suppression de l'appel à lockdown par défaut

* improving readme

* ses@1.14

* add section on securing fillOdtTemplate
2025-09-18 16:15:24 +02:00
David Bruant
98817e9d34 bump readme version 2025-09-18 10:41:35 +02:00
David Bruant
ca40785155 0.28.0 2025-09-18 10:41:10 +02:00
Clémence
8cc74a6fe6
Extract text inside a text:a tag (#21)
* Extract text inside a text:a tag

* Extract text inside a text:a tag and create a test

* Fix test regression

* Use example domain name for mails

* nettoyage console.log

---------

Co-authored-by: David Bruant <davidbruant@protonmail.com>
2025-09-18 10:36:59 +02:00
Clémence Fernandez
0938a97a83 bump readme version 0.27.0 2025-09-16 16:45:34 +02:00
11 changed files with 226 additions and 72 deletions

60
package-lock.json generated
View File

@ -1,17 +1,17 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.27.0", "version": "0.30.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.27.0", "version": "0.30.0",
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.9.8", "@xmldom/xmldom": "^0.9.8",
"@zip.js/zip.js": "^2.7.57", "@zip.js/zip.js": "^2.7.57",
"image-size": "^2.0.2", "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,10 +42,22 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@endo/cache-map": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@endo/cache-map/-/cache-map-1.1.0.tgz",
"integrity": "sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==",
"license": "Apache-2.0"
},
"node_modules/@endo/env-options": { "node_modules/@endo/env-options": {
"version": "1.1.8", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==", "integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==",
"license": "Apache-2.0"
},
"node_modules/@endo/immutable-arraybuffer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@endo/immutable-arraybuffer/-/immutable-arraybuffer-1.1.2.tgz",
"integrity": "sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
@ -3628,12 +3640,14 @@
} }
}, },
"node_modules/ses": { "node_modules/ses": {
"version": "1.12.0", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz", "resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==", "integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@endo/env-options": "^1.1.8" "@endo/cache-map": "^1.1.0",
"@endo/env-options": "^1.1.11",
"@endo/immutable-arraybuffer": "^1.1.2"
} }
}, },
"node_modules/set-blocking": { "node_modules/set-blocking": {
@ -4584,10 +4598,20 @@
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.9"
} }
}, },
"@endo/cache-map": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@endo/cache-map/-/cache-map-1.1.0.tgz",
"integrity": "sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw=="
},
"@endo/env-options": { "@endo/env-options": {
"version": "1.1.8", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==" "integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA=="
},
"@endo/immutable-arraybuffer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@endo/immutable-arraybuffer/-/immutable-arraybuffer-1.1.2.tgz",
"integrity": "sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ=="
}, },
"@jridgewell/gen-mapping": { "@jridgewell/gen-mapping": {
"version": "0.3.2", "version": "0.3.2",
@ -7156,11 +7180,13 @@
} }
}, },
"ses": { "ses": {
"version": "1.12.0", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz", "resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==", "integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
"requires": { "requires": {
"@endo/env-options": "^1.1.8" "@endo/cache-map": "^1.1.0",
"@endo/env-options": "^1.1.11",
"@endo/immutable-arraybuffer": "^1.1.2"
} }
}, },
"set-blocking": { "set-blocking": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.27.0", "version": "0.30.0",
"type": "module", "type": "module",
"exports": "./exports.js", "exports": "./exports.js",
"files": [ "files": [
@ -21,7 +21,7 @@
"test": "ava" "test": "ava"
}, },
"repository": { "repository": {
"url": "https://github.com/odfjs/odfjs.git" "url": "https://source.netsyms.com/PostalPortal/odfjs.git"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
@ -42,6 +42,6 @@
"@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", "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.26.0 npm i https://github.com/odfjs/odfjs.git#v0.30.0
``` ```
@ -30,7 +30,7 @@ import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent}
/** /**
* @param {ArrayBuffer} odsFile - content of an .ods file * @param {ArrayBuffer} odsFile - content of an .ods file
* @return {Promise<any[]>} * @return {Promise<any[]>}
*/ */
async function getFileData(odsFile){ async function getFileData(odsFile){
return getODSTableRawContent(odsFile) return getODSTableRawContent(odsFile)
.then(tableWithoutEmptyRows) .then(tableWithoutEmptyRows)
@ -38,9 +38,9 @@ async function getFileData(odsFile){
} }
``` ```
The return value is an array of objects where The return value is an array of objects where
the **keys** are the column names in the first row and the **keys** are the column names in the first row and
the **values** are automatically converted from the .ods files (which type numbers, strings, booleans and dates) the **values** are automatically converted from the .ods files (which type numbers, strings, booleans and dates)
to the appropriate JavaScript value to the appropriate JavaScript value
@ -88,7 +88,7 @@ odf.js proposes a template syntax
In an .odt file, write the following: In an .odt file, write the following:
```txt ```txt
Hey {nom}! Hey {nom}!
Your birthdate is {dateNaissance} Your birthdate is {dateNaissance}
``` ```
@ -99,8 +99,7 @@ And then run the code:
```js ```js
import {join} from 'node:path'; import {join} from 'node:path';
import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js' import {getOdtTemplate, fillOdtTemplate} from '@odfjs/odfjs'
import {fillOdtTemplate} from '../scripts/node.js'
// replace with your template path // replace with your template path
const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt') const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt')
@ -126,6 +125,19 @@ There are also loops in the form:
They can be used to generate lists or tables in .odt files from data and a template using this syntax They can be used to generate lists or tables in .odt files from data and a template using this syntax
#### Securing calls to fillOdtTemplate
`fillOdtTemplate` evaluate arbitrary JavaScript code in `{#each <collection> as élément}` and `{#if <condition>}` and in `{<expression>}`
By default, `fillOdtTemplate` limits access to global functions to only ECMAScript defaults via the use of [ses' Compartment](https://www.npmjs.com/package/ses#compartment), this prevents naïve data exfiltration
However, `fillOdtTemplate` is vulnerable to [prototype pollution](https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html) inside template code. Two main ways to be secure are:
- control the set of possible templates
- call ses' `lockdown` which freezes Javascript intrinsics before calling `fillOdtTemplate` (this may lead to incompatibilities)
### Demo ### Demo
https://odfjs.github.io/odfjs/ https://odfjs.github.io/odfjs/
@ -146,4 +158,3 @@ npm run dev
I hope to be credited for the work on this repo I hope to be credited for the work on this repo
Everything written by me and contributors to this repo is licenced under **CC0 1.0 (Public Domain)** Everything written by me and contributors to this repo is licenced under **CC0 1.0 (Public Domain)**

View File

@ -6,11 +6,15 @@ import {serializeToString, createDocument} from './DOMUtils.js'
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */ /** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
const stylesXml = `<?xml version="1.0" encoding="UTF-8"?> const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles <office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
office:version="1.2"> office:version="1.2">
<office:styles/> <office:styles>
<style:style style:name="boldcell" style:family="table-cell">
<style:text-properties fo:font-weight="bold"/>
</style:style>
</office:styles>
<office:automatic-styles/> <office:automatic-styles/>
<office:master-styles/> <office:master-styles/>
</office:document-styles>`; </office:document-styles>`;
@ -27,11 +31,11 @@ const manifestXml = `<?xml version="1.0" encoding="UTF-8"?>
* @param {Map<SheetName, SheetRawContent>} sheetsData * @param {Map<SheetName, SheetRawContent>} sheetsData
* @returns {Promise<ArrayBuffer>} * @returns {Promise<ArrayBuffer>}
*/ */
export async function createOdsFile(sheetsData) { export async function createOdsFile(sheetsData, currencyData = null) {
// Create a new zip writer // Create a new zip writer
const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet')); const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet'));
// The “mimetype” file shall be the first file of the zip file. // The “mimetype” file shall be the first file of the zip file.
// It shall not be compressed, and it shall not use an 'extra field' in its header. // It shall not be compressed, and it shall not use an 'extra field' in its header.
// https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813 // https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813
zipWriter.add( zipWriter.add(
@ -45,7 +49,7 @@ export async function createOdsFile(sheetsData) {
} }
); );
const contentXml = generateContentFileXMLString(sheetsData); const contentXml = generateContentFileXMLString(sheetsData, currencyData);
zipWriter.add("content.xml", new TextReader(contentXml), {level: 9}); zipWriter.add("content.xml", new TextReader(contentXml), {level: 9});
zipWriter.add("styles.xml", new TextReader(stylesXml)); zipWriter.add("styles.xml", new TextReader(stylesXml));
@ -60,10 +64,10 @@ export async function createOdsFile(sheetsData) {
/** /**
* Generate the content.xml file with spreadsheet data * Generate the content.xml file with spreadsheet data
* @param {Map<SheetName, SheetRawContent>} sheetsData * @param {Map<SheetName, SheetRawContent>} sheetsData
* @returns {string} * @returns {string}
*/ */
function generateContentFileXMLString(sheetsData) { function generateContentFileXMLString(sheetsData, currencyData) {
const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content'); const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content');
const root = doc.documentElement; const root = doc.documentElement;
@ -75,6 +79,52 @@ function generateContentFileXMLString(sheetsData) {
root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
root.setAttribute('office:version', '1.2'); root.setAttribute('office:version', '1.2');
const styleNode = doc.createElement("office:automatic-styles");
var currencyStyleName = "currencyStyle";
if (currencyData != null) {
currencyStyleName = `currency${currencyData.currencyCode.toUpperCase()}`;
const numberStyle = doc.createElement("number:currency-style");
numberStyle.setAttribute("style:name", currencyStyleName);
const numberCurrencySymbolStyle = doc.createElement("number:currency-symbol");
numberCurrencySymbolStyle.setAttribute("number:language", "en");
numberCurrencySymbolStyle.setAttribute("number:country", currencyData.countryCode.toUpperCase());
numberCurrencySymbolStyle.textContent = currencyData.currencySymbol;
numberStyle.appendChild(numberCurrencySymbolStyle);
const numberCurrencyStyle = doc.createElement("number:number");
numberCurrencyStyle.setAttribute("number:min-integer-digits", "1");
numberCurrencyStyle.setAttribute("number:decimal-places", `${currencyData.decimalPlaces}`);
numberCurrencyStyle.setAttribute("number:min-decimal-places", `${currencyData.decimalPlaces}`);
numberCurrencyStyle.setAttribute("number:grouping", "true");
numberStyle.appendChild(numberCurrencyStyle);
styleNode.appendChild(numberStyle);
const currencyCellStyleNode = doc.createElement("style:style");
currencyCellStyleNode.setAttribute("style:name", "currencycell");
currencyCellStyleNode.setAttribute("style:family", "table-cell");
currencyCellStyleNode.setAttribute("style:data-style-name", currencyStyleName);
const currencyCellTableCellProperties = doc.createElement("style:table-cell-properties");
currencyCellStyleNode.appendChild(currencyCellTableCellProperties);
styleNode.appendChild(currencyCellStyleNode);
}
const boldCellStyleNode = doc.createElement("style:style");
boldCellStyleNode.setAttribute("style:name", "boldcell");
boldCellStyleNode.setAttribute("style:family", "table-cell");
const boldCellTextPropsNode = doc.createElement("style:text-properties");
boldCellTextPropsNode.setAttribute("fo:font-weight", "bold");
boldCellStyleNode.appendChild(boldCellTextPropsNode);
styleNode.appendChild(boldCellStyleNode);
root.appendChild(styleNode);
const bodyNode = doc.createElement('office:body'); const bodyNode = doc.createElement('office:body');
root.appendChild(bodyNode); root.appendChild(bodyNode);
@ -87,8 +137,30 @@ function generateContentFileXMLString(sheetsData) {
tableNode.setAttribute('table:name', sheetName); tableNode.setAttribute('table:name', sheetName);
spreadsheetNode.appendChild(tableNode); spreadsheetNode.appendChild(tableNode);
const columnNode = doc.createElement('table:table-column'); var columnsWidthChars = {};
tableNode.appendChild(columnNode); for (let r = 0; r < sheetData.length; r++) {
for (let c = 0; c < sheetData[r].length; c++) {
var len = ((sheetData[r][c].display ?? sheetData[r][c].value) + "").length;
if (typeof columnsWidthChars[c] == "undefined") {
columnsWidthChars[c] = len;
}
columnsWidthChars[c] = Math.max(columnsWidthChars[c], len);
}
}
for (var prop in columnsWidthChars) {
var columnNode = doc.createElement('table:table-column');
columnNode.setAttribute("table:style-name", "colwidth" + columnsWidthChars[prop]);
tableNode.appendChild(columnNode);
var columnWidthNode = doc.createElement("style:style");
columnWidthNode.setAttribute("style:name", "colwidth" + columnsWidthChars[prop]);
columnWidthNode.setAttribute("style:family", "table-column");
const columnWidthPropsNode = doc.createElement("style:table-column-properties");
columnWidthPropsNode.setAttribute("style:column-width", `${columnsWidthChars[prop] * 0.26}cm`);
columnWidthNode.appendChild(columnWidthPropsNode);
styleNode.appendChild(columnWidthNode);
}
// Iterate through rows // Iterate through rows
sheetData.forEach((row) => { sheetData.forEach((row) => {
@ -101,6 +173,10 @@ function generateContentFileXMLString(sheetsData) {
const cellType = convertCellType(cell.type); const cellType = convertCellType(cell.type);
cellNode.setAttribute('office:value-type', cellType); cellNode.setAttribute('office:value-type', cellType);
if (cell.style && cell.style == "bold") {
cellNode.setAttribute('table:style-name', "boldcell");
}
// Add value attribute based on type // Add value attribute based on type
if (cell.value !== null && cell.value !== undefined) { if (cell.value !== null && cell.value !== undefined) {
switch (cellType) { switch (cellType) {
@ -111,6 +187,14 @@ function generateContentFileXMLString(sheetsData) {
cellNode.setAttribute('office:value', cell.value.toString()); cellNode.setAttribute('office:value', cell.value.toString());
cellNode.setAttribute('office:value-type', 'percentage'); cellNode.setAttribute('office:value-type', 'percentage');
break; break;
case 'currency':
cellNode.setAttribute('office:value', cell.value.toString());
cellNode.setAttribute('office:value-type', 'currency');
if (currencyData != null) {
cellNode.setAttribute("table:style-name", "currencycell");
cellNode.setAttribute('office:currency', currencyData.currencyCode.toUpperCase());
}
break;
case 'date': case 'date':
cellNode.setAttribute('office:date-value', cell.value.toString()); cellNode.setAttribute('office:date-value', cell.value.toString());
break; break;
@ -126,7 +210,11 @@ function generateContentFileXMLString(sheetsData) {
if (cellType !== 'string') { if (cellType !== 'string') {
const textNode = doc.createElement('text:p'); const textNode = doc.createElement('text:p');
textNode.textContent = cell.value.toString(); if (typeof cell.display != "undefined") {
textNode.textContent = cell.display.toString();
} else {
textNode.textContent = cell.value.toString();
}
cellNode.appendChild(textNode); cellNode.appendChild(textNode);
} }
} }
@ -141,7 +229,7 @@ function generateContentFileXMLString(sheetsData) {
/** /**
* Convert cell type to OpenDocument format type * Convert cell type to OpenDocument format type
* @param {SheetCellRawContent['type']} type * @param {SheetCellRawContent['type']} type
* @returns {SheetCellRawContent['type']} * @returns {SheetCellRawContent['type']}
*/ */
function convertCellType(type) { function convertCellType(type) {

View File

@ -799,7 +799,7 @@ export default function fillOdtElementTemplate(rootElements, compartment, addIma
} else { } else {
const imageMarker = findImageMarker(currentNode.data, compartment) const imageMarker = findImageMarker(currentNode.data, compartment)
if (imageMarker){ if (imageMarker){
console.log({imageMarker}, "dans le if imageMarker") //console.log({imageMarker}, "dans le if imageMarker")
if (imageMarker.odfjsImage) { if (imageMarker.odfjsImage) {
const href = addImageToOdtFile(imageMarker.odfjsImage) const href = addImageToOdtFile(imageMarker.odfjsImage)

View File

@ -7,8 +7,6 @@ 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, ODFManifestFileEntry} from '../manifest.js' */

View File

@ -11,14 +11,13 @@ import {parseXML} from './DOMUtils.js'
const TEXT_NODE = 3 const TEXT_NODE = 3
/** /**
* *
* @param {Element} cell * @param {Element} cell
* @returns {string} * @returns {string}
*/ */
function extraxtODSCellText(cell) { function extraxtODSCellText(cell) {
let text = ''; let text = '';
const childNodes = cell.childNodes; const childNodes = cell.childNodes;
for (const child of Array.from(childNodes)) { for (const child of Array.from(childNodes)) {
if (child.nodeType === TEXT_NODE) { if (child.nodeType === TEXT_NODE) {
// Direct text node, append the text directly // Direct text node, append the text directly
@ -34,13 +33,15 @@ 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') {
text += '\n'; // Append newline for <text:line-break /> directly under <table:table-cell> text += '\n'; // Append newline for <text:line-break /> directly under <table:table-cell>
} }
} }
return text.trim(); return text.trim();
} }
@ -126,7 +127,7 @@ export async function getODSTableRawContent(arrayBuffer) {
/** /**
* Converts a cell value to the appropriate JavaScript type based on its cell type. * Converts a cell value to the appropriate JavaScript type based on its cell type.
* @param {SheetCellRawContent} _ * @param {SheetCellRawContent} _
* @returns {number | boolean | string | Date} The converted value. * @returns {number | boolean | string | Date} The converted value.
*/ */
export function convertCellValue({value, type}) { export function convertCellValue({value, type}) {
@ -162,10 +163,10 @@ export function convertCellValue({value, type}) {
/** /**
* @param {unknown} value * @param {unknown} value
* @returns {value is OdfjsImage} * @returns {value is OdfjsImage}
*/ */
export function isOdfjsImage(value) { export function isOdfjsImage(value) {
if (typeof value === 'object' && value!==null if (typeof value === 'object' && value!==null
&& "content" in value && value.content instanceof ArrayBuffer && "content" in value && value.content instanceof ArrayBuffer
&& "fileName" in value && typeof value.fileName === 'string' && "fileName" in value && typeof value.fileName === 'string'
&& "mediaType" in value && typeof value.mediaType === 'string' && "mediaType" in value && typeof value.mediaType === 'string'
@ -182,15 +183,15 @@ export function isOdfjsImage(value) {
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentSheets * @param {Map<SheetName, SheetRawContent>} rawContentSheets
* @returns {Map<SheetName, ReturnType<convertCellValue>[][]>} * @returns {Map<SheetName, ReturnType<convertCellValue>[][]>}
*/ */
export function tableRawContentToValues(rawContentSheets){ export function tableRawContentToValues(rawContentSheets){
return new Map( return new Map(
[...rawContentSheets].map(([sheetName, rawContent]) => { [...rawContentSheets].map(([sheetName, rawContent]) => {
return [ return [
sheetName, sheetName,
rawContent rawContent
.map(row => row.map(c => convertCellValue(c))) .map(row => row.map(c => convertCellValue(c)))
] ]
@ -203,7 +204,7 @@ export function tableRawContentToValues(rawContentSheets){
*/ */
/** /**
* *
* @param {SheetCellRawContent} rawContentCell * @param {SheetCellRawContent} rawContentCell
* @returns {string} * @returns {string}
*/ */
@ -212,8 +213,8 @@ export function cellRawContentToStrings(rawContentCell){
} }
/** /**
* *
* @param {SheetRowRawContent} rawContentRow * @param {SheetRowRawContent} rawContentRow
* @returns {string[]} * @returns {string[]}
*/ */
export function rowRawContentToStrings(rawContentRow){ export function rowRawContentToStrings(rawContentRow){
@ -221,8 +222,8 @@ export function rowRawContentToStrings(rawContentRow){
} }
/** /**
* *
* @param {SheetRawContent} rawContentSheet * @param {SheetRawContent} rawContentSheet
* @returns {string[][]} * @returns {string[][]}
*/ */
export function sheetRawContentToStrings(rawContentSheet){ export function sheetRawContentToStrings(rawContentSheet){
@ -230,8 +231,8 @@ export function sheetRawContentToStrings(rawContentSheet){
} }
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentSheets * @param {Map<SheetName, SheetRawContent>} rawContentSheets
* @returns {Map<SheetName, string[][]>} * @returns {Map<SheetName, string[][]>}
*/ */
export function tableRawContentToStrings(rawContentSheets){ export function tableRawContentToStrings(rawContentSheets){
@ -252,16 +253,16 @@ export function tableRawContentToStrings(rawContentSheets){
/** /**
* This function expects the first row to contain string values which are used as column names * This function expects the first row to contain string values which are used as column names
* It outputs an array of objects which keys are * It outputs an array of objects which keys are
* *
* @param {SheetRawContent} rawContent * @param {SheetRawContent} rawContent
* @returns {any[]} * @returns {any[]}
*/ */
export function sheetRawContentToObjects(rawContent){ export function sheetRawContentToObjects(rawContent){
let [firstRow, ...dataRows] = rawContent let [firstRow, ...dataRows] = rawContent
/** @type {string[]} */ /** @type {string[]} */
const columns = firstRow.map((r, i) => { const columns = firstRow.map((r, i) => {
if (r.value === undefined || r.value === null || r.value === "") { if (r.value === undefined || r.value === null || r.value === "") {
return `Column ${i+1}` return `Column ${i+1}`
@ -283,8 +284,8 @@ export function sheetRawContentToObjects(rawContent){
} }
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentSheets * @param {Map<SheetName, SheetRawContent>} rawContentSheets
* @returns {Map<SheetName, any[]>} * @returns {Map<SheetName, any[]>}
*/ */
export function tableRawContentToObjects(rawContentSheets){ export function tableRawContentToObjects(rawContentSheets){
@ -311,7 +312,7 @@ export function isCellFilled({value}){
} }
/** /**
* @param {SheetRowRawContent} rawContentRow * @param {SheetRowRawContent} rawContentRow
* @returns {boolean} * @returns {boolean}
*/ */
export function isRowNotEmpty(rawContentRow){ export function isRowNotEmpty(rawContentRow){
@ -319,7 +320,7 @@ export function isRowNotEmpty(rawContentRow){
} }
/** /**
* @param {SheetRawContent} sheet * @param {SheetRawContent} sheet
* @returns {SheetRawContent} * @returns {SheetRawContent}
*/ */
export function removeEmptyRowsFromSheet(sheet){ export function removeEmptyRowsFromSheet(sheet){
@ -328,8 +329,8 @@ export function removeEmptyRowsFromSheet(sheet){
/** /**
* *
* @param {Map<SheetName, SheetRawContent>} rawContentTable * @param {Map<SheetName, SheetRawContent>} rawContentTable
* @returns {Map<SheetName, SheetRawContent>} * @returns {Map<SheetName, SheetRawContent>}
*/ */
export function tableWithoutEmptyRows(rawContentTable){ export function tableWithoutEmptyRows(rawContentTable){
@ -338,4 +339,4 @@ export function tableWithoutEmptyRows(rawContentTable){
return [sheetName, removeEmptyRowsFromSheet(rawContent)] return [sheetName, removeEmptyRowsFromSheet(rawContent)]
}) })
) )
} }

View File

@ -9,7 +9,7 @@ import { listZipEntries } from '../helpers/zip-analysis.js';
import { getContentDocument } from '../../scripts/odf/odt/getOdtTextContent.js'; import { getContentDocument } from '../../scripts/odf/odt/getOdtTextContent.js';
test.skip('template filling preserves images', async t => { test('template filling preserves images', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/template-avec-image.odt') const templatePath = join(import.meta.dirname, '../fixtures/template-avec-image.odt')
const data = { const data = {

BIN
tests/fixtures/cellule avec style.ods vendored Normal file

Binary file not shown.

BIN
tests/fixtures/cellules avec emails.ods vendored Normal file

Binary file not shown.

View File

@ -55,4 +55,34 @@ Pourquoi t'aimeraient-ils seulement comme tu es ? (hein)
Si t'es pas comme eux quand t'es naturel` 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');
});