Compare commits
No commits in common. "main" and "v0.9.0" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
@ -2,8 +2,4 @@ node_modules/
|
||||
|
||||
build/*
|
||||
|
||||
.~lock*
|
||||
**/*(Copie)*
|
||||
**/*(Copy)*
|
||||
|
||||
stats.html
|
||||
.~lock*
|
||||
29
exports.js
29
exports.js
@ -1,29 +0,0 @@
|
||||
//@ts-check
|
||||
|
||||
export {default as fillOdtTemplate} from './scripts/odf/templating/fillOdtTemplate.js'
|
||||
export {getOdtTextContent} from './scripts/odf/odt/getOdtTextContent.js'
|
||||
|
||||
export { createOdsFile } from './scripts/createOdsFile.js'
|
||||
|
||||
export {
|
||||
getODSTableRawContent,
|
||||
|
||||
// table-level exports
|
||||
tableWithoutEmptyRows,
|
||||
tableRawContentToValues,
|
||||
tableRawContentToStrings,
|
||||
tableRawContentToObjects,
|
||||
|
||||
// sheet-level exports
|
||||
sheetRawContentToObjects,
|
||||
sheetRawContentToStrings,
|
||||
|
||||
// row-level exports
|
||||
rowRawContentToStrings,
|
||||
isRowNotEmpty,
|
||||
|
||||
// cell-level exports
|
||||
cellRawContentToStrings,
|
||||
convertCellValue
|
||||
} from './scripts/shared.js'
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<title>Upload ods file</title>
|
||||
<title>Upload ods/xlsx</title>
|
||||
|
||||
<meta name="description" content=" ">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
480
package-lock.json
generated
480
package-lock.json
generated
@ -1,17 +1,16 @@
|
||||
{
|
||||
"name": "@odfjs/odfjs",
|
||||
"version": "0.30.0",
|
||||
"name": "ods-xlsx",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@odfjs/odfjs",
|
||||
"version": "0.30.0",
|
||||
"name": "ods-xlsx",
|
||||
"version": "0.9.0",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"@zip.js/zip.js": "^2.7.57",
|
||||
"image-size": "^2.0.2",
|
||||
"ses": "^1.14.0"
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"unzipit": "^1.4.3",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
@ -23,7 +22,6 @@
|
||||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"rollup-plugin-svelte": "^7.1.6",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.58.3",
|
||||
"svelte": "^4.2.9",
|
||||
"svelte-preprocess": "^5.1.3"
|
||||
@ -42,24 +40,6 @@
|
||||
"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": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
|
||||
"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"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
@ -624,23 +604,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
|
||||
"integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==",
|
||||
"license": "MIT",
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.7.57",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz",
|
||||
"integrity": "sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"bun": ">=0.7.0",
|
||||
"deno": ">=1.0.0",
|
||||
"node": ">=16.5.0"
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
@ -682,6 +650,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -1095,6 +1072,19 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@ -1280,6 +1270,15 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@ -1383,6 +1382,18 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
@ -1438,16 +1449,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
@ -1710,6 +1711,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
@ -2198,18 +2208,6 @@
|
||||
"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": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
|
||||
@ -2325,22 +2323,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -2458,19 +2440,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@ -2975,24 +2944,6 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-lazy-prop": "^2.0.0",
|
||||
"is-docker": "^2.1.1",
|
||||
"is-wsl": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
@ -3477,60 +3428,6 @@
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer": {
|
||||
"version": "5.14.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"open": "^8.4.0",
|
||||
"picomatch": "^4.0.2",
|
||||
"source-map": "^0.7.4",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"bin": {
|
||||
"rollup-plugin-visualizer": "dist/bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rolldown": "1.x",
|
||||
"rollup": "2.x || 3.x || 4.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rolldown": {
|
||||
"optional": true
|
||||
},
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@ -3639,17 +3536,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ses": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
|
||||
"integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@endo/cache-map": "^1.1.0",
|
||||
"@endo/env-options": "^1.1.11",
|
||||
"@endo/immutable-arraybuffer": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@ -3833,6 +3719,18 @@
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -4233,6 +4131,17 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipit": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/unzipit/-/unzipit-1.4.3.tgz",
|
||||
"integrity": "sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==",
|
||||
"dependencies": {
|
||||
"uzip-module": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
@ -4245,6 +4154,11 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uzip-module": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
|
||||
"integrity": "sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA=="
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@ -4363,6 +4277,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@ -4494,6 +4426,27 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
@ -4598,21 +4551,6 @@
|
||||
"@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": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
|
||||
"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": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
@ -4980,14 +4918,9 @@
|
||||
}
|
||||
},
|
||||
"@xmldom/xmldom": {
|
||||
"version": "0.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
|
||||
"integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A=="
|
||||
},
|
||||
"@zip.js/zip.js": {
|
||||
"version": "2.7.57",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz",
|
||||
"integrity": "sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA=="
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
@ -5017,6 +4950,11 @@
|
||||
"acorn": "^8.11.0"
|
||||
}
|
||||
},
|
||||
"adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -5327,6 +5265,15 @@
|
||||
"nofilter": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"requires": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@ -5472,6 +5419,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@ -5559,6 +5511,11 @@
|
||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
|
||||
},
|
||||
"css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
@ -5602,12 +5559,6 @@
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"dev": true
|
||||
},
|
||||
"define-lazy-prop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
||||
"dev": true
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
@ -5798,6 +5749,11 @@
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"dev": true
|
||||
},
|
||||
"frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
@ -6166,11 +6122,6 @@
|
||||
"integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==",
|
||||
"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": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
|
||||
@ -6256,12 +6207,6 @@
|
||||
"integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
|
||||
"dev": true
|
||||
},
|
||||
"is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -6346,15 +6291,6 @@
|
||||
"integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==",
|
||||
"dev": true
|
||||
},
|
||||
"is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@ -6730,17 +6666,6 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-lazy-prop": "^2.0.0",
|
||||
"is-docker": "^2.1.1",
|
||||
"is-wsl": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
@ -7088,32 +7013,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"rollup-plugin-visualizer": {
|
||||
"version": "5.14.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"open": "^8.4.0",
|
||||
"picomatch": "^4.0.2",
|
||||
"source-map": "^0.7.4",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@ -7179,16 +7078,6 @@
|
||||
"type-fest": "^0.13.1"
|
||||
}
|
||||
},
|
||||
"ses": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
|
||||
"integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
|
||||
"requires": {
|
||||
"@endo/cache-map": "^1.1.0",
|
||||
"@endo/env-options": "^1.1.11",
|
||||
"@endo/immutable-arraybuffer": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@ -7335,6 +7224,14 @@
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true
|
||||
},
|
||||
"ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"requires": {
|
||||
"frac": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -7598,6 +7495,14 @@
|
||||
"qs": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"unzipit": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/unzipit/-/unzipit-1.4.3.tgz",
|
||||
"integrity": "sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==",
|
||||
"requires": {
|
||||
"uzip-module": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
@ -7610,6 +7515,11 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"uzip-module": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
|
||||
"integrity": "sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA=="
|
||||
},
|
||||
"validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@ -7709,6 +7619,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
|
||||
},
|
||||
"word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@ -7808,6 +7728,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"requires": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
28
package.json
28
package.json
@ -1,18 +1,9 @@
|
||||
{
|
||||
"name": "@odfjs/odfjs",
|
||||
"version": "0.30.0",
|
||||
"name": "ods-xlsx",
|
||||
"version": "0.9.0",
|
||||
"type": "module",
|
||||
"exports": "./exports.js",
|
||||
"files": [
|
||||
"exports.js",
|
||||
"scripts"
|
||||
],
|
||||
"imports": {
|
||||
"#DOM": {
|
||||
"node": "./scripts/DOM/node.js",
|
||||
"browser": "./scripts/DOM/browser.js"
|
||||
}
|
||||
},
|
||||
"main": "./scripts/node.js",
|
||||
"browser": "./scripts/browser.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "npm-run-all --parallel dev:* start",
|
||||
@ -20,9 +11,6 @@
|
||||
"start": "http-server -c-1 .",
|
||||
"test": "ava"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://source.netsyms.com/PostalPortal/odfjs.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
@ -33,15 +21,13 @@
|
||||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"rollup-plugin-svelte": "^7.1.6",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.58.3",
|
||||
"svelte": "^4.2.9",
|
||||
"svelte-preprocess": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"@zip.js/zip.js": "^2.7.57",
|
||||
"image-size": "^2.0.2",
|
||||
"ses": "^1.14.0"
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"unzipit": "^1.4.3",
|
||||
"xlsx": "^0.18.5"
|
||||
}
|
||||
}
|
||||
|
||||
109
readme.md
109
readme.md
@ -1,16 +1,6 @@
|
||||
# @odfjs/odfjs
|
||||
# ods-xlsx
|
||||
|
||||
Small lib to parse/understand .odf files (.odt, .ods) in the browser and node.js
|
||||
|
||||
|
||||
## Rough roadmap
|
||||
|
||||
- [x] add odt templating
|
||||
- [x] remove support for xlsx
|
||||
- [ ] add a .ods minifyer
|
||||
- [ ] add a generic .ods visualizer
|
||||
- [ ] move to a dedicated odf docs org
|
||||
- [ ] add a quick .odt visualiser (maybe converting to markdown first?)
|
||||
Small lib to parse/understand .ods and .xsls files in the browser and node.js
|
||||
|
||||
|
||||
## Usage
|
||||
@ -18,36 +8,38 @@ Small lib to parse/understand .odf files (.odt, .ods) in the browser and node.js
|
||||
### Install
|
||||
|
||||
```sh
|
||||
npm i https://github.com/odfjs/odfjs.git#v0.30.0
|
||||
npm i https://github.com/DavidBruant/ods-xlsx.git#v0.9.0
|
||||
```
|
||||
|
||||
|
||||
### Basic - reading an ods file
|
||||
### Usage
|
||||
|
||||
#### Basic - reading an ods/xlsx file
|
||||
|
||||
```js
|
||||
import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent} from '@odfjs/odfjs'
|
||||
import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent} from 'ods-xlsx'
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} odsFile - content of an .ods file
|
||||
* @param {File} file - an .ods file like the ones you get from an <input type=file>
|
||||
* @return {Promise<any[]>}
|
||||
*/
|
||||
async function getFileData(odsFile){
|
||||
return getODSTableRawContent(odsFile)
|
||||
*/
|
||||
async function getFileData(file){
|
||||
return tableRawContent
|
||||
.then(tableWithoutEmptyRows)
|
||||
.then(tableRawContentToObjects)
|
||||
}
|
||||
```
|
||||
|
||||
The return value is an array of objects where
|
||||
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 return value is an array of objects where
|
||||
the **keys** are the column names in the first row and
|
||||
the **values** are automatically converted from the .ods or .xlsx files (which type numbers, strings, booleans and dates)
|
||||
to the appropriate JavaScript value
|
||||
|
||||
|
||||
### Basic - creating an ods file
|
||||
#### Basic - creating an ods file
|
||||
|
||||
```js
|
||||
import {createOdsFile} from '@odfjs/odfjs'
|
||||
import {createOdsFile} from 'ods-xlsx'
|
||||
|
||||
const content = new Map([
|
||||
[
|
||||
@ -77,70 +69,14 @@ const ods = await createOdsFile(content)
|
||||
// ods is an ArrayBuffer representing an ods file with the content described by the Map
|
||||
```
|
||||
|
||||
(and there is a tool to test file creation:
|
||||
`node tools/create-an-ods-file.js > yo.ods`)
|
||||
|
||||
|
||||
### Filling an .odt template
|
||||
|
||||
odf.js proposes a template syntax
|
||||
|
||||
In an .odt file, write the following:
|
||||
|
||||
```txt
|
||||
Hey {nom}!
|
||||
|
||||
Your birthdate is {dateNaissance}
|
||||
```
|
||||
|
||||
And then run the code:
|
||||
|
||||
|
||||
```js
|
||||
import {join} from 'node:path';
|
||||
|
||||
import {getOdtTemplate, fillOdtTemplate} from '@odfjs/odfjs'
|
||||
|
||||
// replace with your template path
|
||||
const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt')
|
||||
const data = {
|
||||
nom: 'David Bruant',
|
||||
dateNaissance: '8 mars 1987'
|
||||
}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
process.stdout.write(new Uint8Array(odtResult))
|
||||
```
|
||||
|
||||
There are also loops in the form:
|
||||
|
||||
```txt
|
||||
- {#each listeCourses as élément}
|
||||
- {élément}
|
||||
- {/each}
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
|
||||
#### Low-level
|
||||
|
||||
See exports
|
||||
|
||||
### Demo
|
||||
|
||||
https://odfjs.github.io/odfjs/
|
||||
https://davidbruant.github.io/ods-xlsx/
|
||||
|
||||
|
||||
## Local dev
|
||||
@ -151,10 +87,13 @@ npm run dev
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Expectations and licence
|
||||
|
||||
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)**
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
Svelte and rollup are **MIT**-licence
|
||||
|
||||
@ -4,7 +4,6 @@ import commonjs from '@rollup/plugin-commonjs';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import sveltePreprocess from 'svelte-preprocess'
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
@ -38,7 +37,6 @@ export default {
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
visualizer(),
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
//production && terser()
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
<script>
|
||||
import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent, createOdsFile} from '../exports.js'
|
||||
//@ts-check
|
||||
|
||||
/** @import {SheetName, SheetRawContent} from './types.js' */
|
||||
import {tableRawContentToObjects, tableWithoutEmptyRows, getODSTableRawContent, getXLSXTableRawContent} from './browser.js'
|
||||
|
||||
const ODS_TYPE = "application/vnd.oasis.opendocument.spreadsheet";
|
||||
|
||||
|
||||
const XLSX_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
|
||||
/**
|
||||
*
|
||||
@ -16,6 +15,9 @@
|
||||
if(file.type === ODS_TYPE)
|
||||
return getODSTableRawContent(await file.arrayBuffer())
|
||||
|
||||
if(file.type === XLSX_TYPE)
|
||||
return getXLSXTableRawContent(await file.arrayBuffer())
|
||||
|
||||
throw new TypeError(`Unsupported file type: ${file.type} (${file.name})`)
|
||||
}
|
||||
|
||||
@ -29,18 +31,15 @@
|
||||
$: tableObjectSheets = tableRawContent && tableRawContent.then(tableWithoutEmptyRows).then(tableRawContentToObjects) || []
|
||||
$: Promise.resolve(tableObjectSheets).then(x => console.log('tableObjectSheets', x))
|
||||
|
||||
// ligne inutile qui utilise createOdsFile pour l'importer dans le bundle
|
||||
$: tableRawContent && tableRawContent.then(createOdsFile).then(ab => console.log('length', ab.byteLength))
|
||||
|
||||
</script>
|
||||
|
||||
<h1>Import fichier .ods</h1>
|
||||
<h1>Import fichier .ods et .xslx</h1>
|
||||
|
||||
<section>
|
||||
<h2>Import</h2>
|
||||
<label>
|
||||
Fichier à importer:
|
||||
<input bind:files type="file" id="file-input" accept="{ ['.ods', ODS_TYPE].join(',') }" />
|
||||
<input bind:files type="file" id="file-input" accept="{ ['.ods', '.xlsx', ODS_TYPE, XLSX_TYPE].join(',') }" />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
|
||||
//console.info('DOM implementation in browser')
|
||||
|
||||
/** @type { typeof DOMImplementation.prototype.createDocument } */
|
||||
export function createDocument(...args){
|
||||
// @ts-ignore
|
||||
return document.implementation.createDocument(...args)
|
||||
}
|
||||
|
||||
export const DOMParser = window.DOMParser
|
||||
export const XMLSerializer = window.XMLSerializer
|
||||
export const Node = window.Node
|
||||
@ -1,17 +0,0 @@
|
||||
import { DOMImplementation } from "@xmldom/xmldom"
|
||||
|
||||
//console.info('DOM implementation in Node.js based on xmldom')
|
||||
|
||||
const implementation = new DOMImplementation()
|
||||
|
||||
/** @type { typeof DOMImplementation.prototype.createDocument } */
|
||||
export function createDocument(...args){
|
||||
// @ts-ignore
|
||||
return implementation.createDocument(...args)
|
||||
}
|
||||
|
||||
export {
|
||||
DOMParser,
|
||||
XMLSerializer,
|
||||
Node
|
||||
} from "@xmldom/xmldom"
|
||||
@ -1,95 +0,0 @@
|
||||
import {DOMParser, XMLSerializer} from '#DOM'
|
||||
|
||||
/*
|
||||
Since we're using xmldom in Node.js context, the entire DOM API is not implemented
|
||||
Functions here are helpers whild xmldom becomes more complete
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {Document}
|
||||
*/
|
||||
export function parseXML(str){
|
||||
return (new DOMParser()).parseFromString(str, 'application/xml');
|
||||
}
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
|
||||
/** @type { typeof XMLSerializer.prototype.serializeToString } */
|
||||
export function serializeToString(node){
|
||||
return serializer.serializeToString(node)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Traverses a DOM tree starting from the given node and applies the visit function
|
||||
* to each Element node encountered in tree order (depth-first).
|
||||
*
|
||||
* This should probably be replace by the TreeWalker API when implemented by xmldom
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {(n : Node) => void} visit
|
||||
*/
|
||||
export function traverse(node, visit) {
|
||||
//console.log('traverse', node.nodeType, node.nodeName)
|
||||
|
||||
for (const child of Array.from(node.childNodes)) {
|
||||
traverse(child, visit);
|
||||
}
|
||||
|
||||
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 {
|
||||
DOMParser,
|
||||
XMLSerializer,
|
||||
createDocument,
|
||||
Node
|
||||
} from '#DOM'
|
||||
51
scripts/browser.js
Normal file
51
scripts/browser.js
Normal file
@ -0,0 +1,51 @@
|
||||
//@ts-check
|
||||
|
||||
import {
|
||||
_getODSTableRawContent,
|
||||
_getXLSXTableRawContent
|
||||
} from './shared.js'
|
||||
|
||||
|
||||
function parseXML(str){
|
||||
return (new DOMParser()).parseFromString(str, 'application/xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} odsArrBuff
|
||||
* @returns {ReturnType<_getODSTableRawContent>}
|
||||
*/
|
||||
export function getODSTableRawContent(odsArrBuff){
|
||||
return _getODSTableRawContent(odsArrBuff, parseXML)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} xlsxArrBuff
|
||||
* @returns {ReturnType<_getXLSXTableRawContent>}
|
||||
*/
|
||||
export function getXLSXTableRawContent(xlsxArrBuff){
|
||||
return _getXLSXTableRawContent(xlsxArrBuff, parseXML)
|
||||
}
|
||||
|
||||
|
||||
export {createOdsFile} from './createOdsFile.js'
|
||||
|
||||
export {
|
||||
// table-level exports
|
||||
tableWithoutEmptyRows,
|
||||
tableRawContentToValues,
|
||||
tableRawContentToStrings,
|
||||
tableRawContentToObjects,
|
||||
|
||||
// sheet-level exports
|
||||
sheetRawContentToObjects,
|
||||
sheetRawContentToStrings,
|
||||
|
||||
// row-level exports
|
||||
rowRawContentToStrings,
|
||||
isRowNotEmpty,
|
||||
|
||||
// cell-level exports
|
||||
cellRawContentToStrings,
|
||||
convertCellValue
|
||||
} from './shared.js'
|
||||
|
||||
@ -1,251 +1,27 @@
|
||||
import { ZipWriter, BlobWriter, TextReader } from '@zip.js/zip.js';
|
||||
//@ts-check
|
||||
|
||||
import {serializeToString, createDocument} from './DOMUtils.js'
|
||||
import XLSX from 'xlsx'
|
||||
|
||||
import {tableRawContentToValues} from './shared.js'
|
||||
|
||||
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
|
||||
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent} from './types.js' */
|
||||
|
||||
const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document-styles
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||||
office:version="1.2">
|
||||
<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:master-styles/>
|
||||
</office:document-styles>`;
|
||||
|
||||
const manifestXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest manifest:version="1.2" xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
||||
<manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.spreadsheet" manifest:full-path="/"/>
|
||||
<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml"/>
|
||||
<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="styles.xml"/>
|
||||
</manifest:manifest>`;
|
||||
const officeVersion = '1.2'
|
||||
|
||||
/**
|
||||
* Crée un fichier .ods à partir d'un Map de feuilles de calcul
|
||||
* @param {Map<SheetName, SheetRawContent>} sheetsData
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*/
|
||||
export async function createOdsFile(sheetsData, currencyData = null) {
|
||||
// Create a new zip writer
|
||||
const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet'));
|
||||
export async function createOdsFile(sheetsData) {
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
// 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.
|
||||
// https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813
|
||||
zipWriter.add(
|
||||
"mimetype",
|
||||
new TextReader("application/vnd.oasis.opendocument.spreadsheet"),
|
||||
{
|
||||
compressionMethod: 0,
|
||||
level: 0,
|
||||
dataDescriptor: false,
|
||||
extendedTimestamp: false,
|
||||
}
|
||||
);
|
||||
const sheetsDataValues = tableRawContentToValues(sheetsData)
|
||||
|
||||
const contentXml = generateContentFileXMLString(sheetsData, currencyData);
|
||||
zipWriter.add("content.xml", new TextReader(contentXml), {level: 9});
|
||||
|
||||
zipWriter.add("styles.xml", new TextReader(stylesXml));
|
||||
|
||||
zipWriter.add('META-INF/manifest.xml', new TextReader(manifestXml));
|
||||
|
||||
// Close the zip writer and get the ArrayBuffer
|
||||
const zipFile = await zipWriter.close();
|
||||
return zipFile.arrayBuffer();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate the content.xml file with spreadsheet data
|
||||
* @param {Map<SheetName, SheetRawContent>} sheetsData
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateContentFileXMLString(sheetsData, currencyData) {
|
||||
const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content');
|
||||
const root = doc.documentElement;
|
||||
|
||||
// Set up namespaces
|
||||
root.setAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
|
||||
root.setAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
|
||||
root.setAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
|
||||
root.setAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
|
||||
root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
|
||||
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);
|
||||
for(const [sheetName, table] of sheetsDataValues){
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(table);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
}
|
||||
|
||||
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');
|
||||
root.appendChild(bodyNode);
|
||||
|
||||
const spreadsheetNode = doc.createElement('office:spreadsheet');
|
||||
bodyNode.appendChild(spreadsheetNode);
|
||||
|
||||
// Iterate through sheets
|
||||
sheetsData.forEach((sheetData, sheetName) => {
|
||||
const tableNode = doc.createElement('table:table');
|
||||
tableNode.setAttribute('table:name', sheetName);
|
||||
spreadsheetNode.appendChild(tableNode);
|
||||
|
||||
var columnsWidthChars = {};
|
||||
for (let r = 0; r < sheetData.length; r++) {
|
||||
for (let c = 0; c < sheetData[r].length; c++) {
|
||||
var len = ((sheetData[r][c].display ?? sheetData[r][c].value) + "").length;
|
||||
if (typeof columnsWidthChars[c] == "undefined") {
|
||||
columnsWidthChars[c] = len;
|
||||
}
|
||||
columnsWidthChars[c] = Math.max(columnsWidthChars[c], len);
|
||||
}
|
||||
}
|
||||
|
||||
for (var prop in columnsWidthChars) {
|
||||
var columnNode = doc.createElement('table:table-column');
|
||||
columnNode.setAttribute("table:style-name", "colwidth" + columnsWidthChars[prop]);
|
||||
tableNode.appendChild(columnNode);
|
||||
|
||||
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
|
||||
sheetData.forEach((row) => {
|
||||
const rowNode = doc.createElement('table:table-row');
|
||||
tableNode.appendChild(rowNode);
|
||||
|
||||
// Iterate through cells in row
|
||||
row.forEach((cell) => {
|
||||
const cellNode = doc.createElement('table:table-cell');
|
||||
const cellType = convertCellType(cell.type);
|
||||
cellNode.setAttribute('office:value-type', cellType);
|
||||
|
||||
if (cell.style && cell.style == "bold") {
|
||||
cellNode.setAttribute('table:style-name', "boldcell");
|
||||
}
|
||||
|
||||
// Add value attribute based on type
|
||||
if (cell.value !== null && cell.value !== undefined) {
|
||||
switch (cellType) {
|
||||
case 'float':
|
||||
cellNode.setAttribute('office:value', cell.value.toString());
|
||||
break;
|
||||
case 'percentage':
|
||||
cellNode.setAttribute('office:value', cell.value.toString());
|
||||
cellNode.setAttribute('office:value-type', 'percentage');
|
||||
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':
|
||||
cellNode.setAttribute('office:date-value', cell.value.toString());
|
||||
break;
|
||||
case 'boolean':
|
||||
cellNode.setAttribute('office:boolean-value', cell.value ? 'true' : 'false');
|
||||
break;
|
||||
default:
|
||||
const textNode = doc.createElement('text:p');
|
||||
textNode.textContent = cell.value.toString();
|
||||
cellNode.appendChild(textNode);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cellType !== 'string') {
|
||||
const textNode = doc.createElement('text:p');
|
||||
if (typeof cell.display != "undefined") {
|
||||
textNode.textContent = cell.display.toString();
|
||||
} else {
|
||||
textNode.textContent = cell.value.toString();
|
||||
}
|
||||
cellNode.appendChild(textNode);
|
||||
}
|
||||
}
|
||||
|
||||
rowNode.appendChild(cellNode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return serializeToString(doc);
|
||||
return XLSX.write(workbook, {bookType: 'ods', type: 'array'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert cell type to OpenDocument format type
|
||||
* @param {SheetCellRawContent['type']} type
|
||||
* @returns {SheetCellRawContent['type']}
|
||||
*/
|
||||
function convertCellType(type) {
|
||||
const typeMap = {
|
||||
'float': 'float',
|
||||
'percentage': 'percentage',
|
||||
'currency': 'currency',
|
||||
'date': 'date',
|
||||
'time': 'time',
|
||||
'boolean': 'boolean',
|
||||
'string': 'string',
|
||||
'n': 'float',
|
||||
's': 'string',
|
||||
'd': 'date',
|
||||
'b': 'boolean'
|
||||
};
|
||||
return typeMap[type] || 'string';
|
||||
}
|
||||
|
||||
|
||||
53
scripts/node.js
Normal file
53
scripts/node.js
Normal file
@ -0,0 +1,53 @@
|
||||
//@ts-check
|
||||
|
||||
import {DOMParser} from '@xmldom/xmldom'
|
||||
|
||||
import {
|
||||
_getODSTableRawContent,
|
||||
_getXLSXTableRawContent
|
||||
} from './shared.js'
|
||||
|
||||
|
||||
function parseXML(str){
|
||||
return (new DOMParser()).parseFromString(str, 'application/xml');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} odsArrBuff
|
||||
* @returns {ReturnType<_getODSTableRawContent>}
|
||||
*/
|
||||
export function getODSTableRawContent(odsArrBuff){
|
||||
return _getODSTableRawContent(odsArrBuff, parseXML)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} xlsxArrBuff
|
||||
* @returns {ReturnType<_getXLSXTableRawContent>}
|
||||
*/
|
||||
export function getXLSXTableRawContent(xlsxArrBuff){
|
||||
return _getXLSXTableRawContent(xlsxArrBuff, parseXML)
|
||||
}
|
||||
|
||||
export {createOdsFile} from './createOdsFile.js'
|
||||
|
||||
export {
|
||||
// table-level exports
|
||||
tableWithoutEmptyRows,
|
||||
tableRawContentToValues,
|
||||
tableRawContentToStrings,
|
||||
tableRawContentToObjects,
|
||||
|
||||
// sheet-level exports
|
||||
sheetRawContentToObjects,
|
||||
sheetRawContentToStrings,
|
||||
|
||||
// row-level exports
|
||||
rowRawContentToStrings,
|
||||
isRowNotEmpty,
|
||||
|
||||
// cell-level exports
|
||||
cellRawContentToStrings,
|
||||
convertCellValue
|
||||
} from './shared.js'
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
|
||||
/*
|
||||
As specified by https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752825_826425813
|
||||
*/
|
||||
|
||||
/** @typedef {'application/vnd.oasis.opendocument.text' | 'application/vnd.oasis.opendocument.spreadsheet'} ODFMediaType */
|
||||
|
||||
/** @typedef {'1.2' | '1.3' | '1.4'} ODFVersion */
|
||||
|
||||
/**
|
||||
* @typedef ODFManifestFileEntry
|
||||
* @prop {string} fullPath
|
||||
* @prop {string} mediaType
|
||||
* @prop {string} [version]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ODFManifest
|
||||
* @prop {ODFMediaType} mediaType
|
||||
* @prop {ODFVersion} version
|
||||
* @prop {Map<ODFManifestFileEntry['fullPath'], ODFManifestFileEntry>} fileEntries
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ODFManifestFileEntry} fileEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
function makeFileEntry({fullPath, mediaType}){
|
||||
return `<manifest:file-entry manifest:full-path="${fullPath}" manifest:media-type="${mediaType}"/>`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ODFManifest} odfManifest
|
||||
* @returns {string}
|
||||
*/
|
||||
export function makeManifestFile({fileEntries, mediaType, version}){
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="${version}">
|
||||
<manifest:file-entry manifest:full-path="/" manifest:version="${version}" manifest:media-type="${mediaType}"/>
|
||||
${[...fileEntries.values()].map(makeFileEntry).join('\n')}
|
||||
</manifest:manifest>`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Document} manifestDoc
|
||||
* @returns {ODFManifest}
|
||||
*/
|
||||
export function getManifestFileData(manifestDoc){
|
||||
/** @type {Partial<ReturnType<getManifestFileData>>} */
|
||||
const manifestData = {
|
||||
fileEntries: new Map()
|
||||
}
|
||||
|
||||
const manifestEl = manifestDoc.getElementsByTagName('manifest:manifest')[0]
|
||||
/** @type {ODFVersion} */
|
||||
// @ts-ignore
|
||||
const version = manifestEl.getAttribute('manifest:version');
|
||||
if(!version){
|
||||
throw new Error(`Missing version attibute in manifest:manifest element of manifest.xml file`)
|
||||
}
|
||||
|
||||
manifestData.version = version
|
||||
|
||||
const manifestEntryEls = manifestEl.getElementsByTagName('manifest:file-entry')
|
||||
|
||||
for(const manifestEntryEl of Array.from(manifestEntryEls)){
|
||||
/** @type {ODFManifestFileEntry} */
|
||||
const odfManifestFileEntry = {
|
||||
fullPath: '',
|
||||
mediaType: ''
|
||||
}
|
||||
|
||||
const fullPath = manifestEntryEl.getAttribute('manifest:full-path')
|
||||
if(!fullPath){
|
||||
throw new Error(`Missing manifest:full-path attribute in manifest entry`)
|
||||
}
|
||||
odfManifestFileEntry.fullPath = fullPath
|
||||
|
||||
const mediaType = manifestEntryEl.getAttribute('manifest:media-type')
|
||||
if(!mediaType){
|
||||
throw new Error(`Missing manifest:media-type attribute in manifest entry for '${fullPath}'`)
|
||||
}
|
||||
odfManifestFileEntry.mediaType = mediaType
|
||||
|
||||
if(fullPath === '/'){
|
||||
// @ts-ignore
|
||||
manifestData.mediaType = mediaType
|
||||
}
|
||||
|
||||
const version = manifestEntryEl.getAttribute('manifest:version')
|
||||
if(version){
|
||||
odfManifestFileEntry.version = version
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
manifestData.fileEntries.set(fullPath, odfManifestFileEntry)
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
return manifestData
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
import { ZipReader, Uint8ArrayReader, TextWriter } from '@zip.js/zip.js';
|
||||
import {parseXML, Node} from '../../DOMUtils.js'
|
||||
|
||||
/** @import {ODTFile} from '../templating/fillOdtTemplate.js' */
|
||||
|
||||
/**
|
||||
* @param {ODTFile} odtFile
|
||||
* @returns {Promise<Document>}
|
||||
*/
|
||||
export async function getContentDocument(odtFile) {
|
||||
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtFile)));
|
||||
|
||||
const entries = await reader.getEntries();
|
||||
|
||||
const contentEntry = entries.find(entry => entry.filename === 'content.xml');
|
||||
|
||||
if (!contentEntry) {
|
||||
throw new Error('No content.xml found in the ODT file');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const contentText = await contentEntry.getData(new TextWriter());
|
||||
await reader.close();
|
||||
|
||||
return parseXML(contentText)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Document} odtDocument
|
||||
* @returns {Element}
|
||||
*/
|
||||
function getODTTextElement(odtDocument) {
|
||||
return odtDocument.getElementsByTagName('office:body')[0]
|
||||
.getElementsByTagName('office:text')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts plain text content from an ODT file, preserving line breaks
|
||||
* @param {ArrayBuffer} odtFile - The ODT file as an ArrayBuffer
|
||||
* @returns {Promise<string>} Extracted text content
|
||||
*/
|
||||
export async function getOdtTextContent(odtFile) {
|
||||
const contentDocument = await getContentDocument(odtFile)
|
||||
const odtTextElement = getODTTextElement(contentDocument)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} element
|
||||
* @returns {string}
|
||||
*/
|
||||
function getElementTextContent(element){
|
||||
//console.log('tagName', element.tagName)
|
||||
if(element.tagName === 'text:h' || element.tagName === 'text:p')
|
||||
return element.textContent + '\n'
|
||||
else{
|
||||
const descendantTexts = Array.from(element.childNodes)
|
||||
.filter(n => n.nodeType === Node.ELEMENT_NODE)
|
||||
.map(getElementTextContent)
|
||||
|
||||
if(element.tagName === 'text:list-item')
|
||||
return `- ${descendantTexts.join('')}`
|
||||
|
||||
return descendantTexts.join('')
|
||||
}
|
||||
}
|
||||
|
||||
return getElementTextContent(odtTextElement)
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} path
|
||||
* @returns {Promise<ODTFile>}
|
||||
*/
|
||||
export async function getOdtTemplate(path) {
|
||||
const fileBuffer = await readFile(path)
|
||||
return fileBuffer.buffer
|
||||
}
|
||||
@ -1,875 +0,0 @@
|
||||
import {traverse, Node, getAncestors, findCommonAncestor} from "../../DOMUtils.js";
|
||||
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, imageMarkerRegex, variableRegex} from './markers.js'
|
||||
import {isOdfjsImage} from "../../shared.js"
|
||||
import imageSize from "image-size";
|
||||
/** @import {OdfjsImage} from "../../types.js" */
|
||||
|
||||
/**
|
||||
* @typedef TextPlaceToFill
|
||||
* @property { {expression: string, replacedString:string}[] } expressions
|
||||
* @property {() => void} fill
|
||||
*/
|
||||
|
||||
class TemplateDOMBranch{
|
||||
/** @type {Node} */
|
||||
#branchBaseNode
|
||||
|
||||
/** @type {Node} */
|
||||
#leafNode
|
||||
|
||||
// ancestors with this.#ancestors[0] === this.#branchBaseNode and this.#ancestors.at(-1) === this.#leafNode
|
||||
/** @type {Node[]} */
|
||||
#ancestors
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Node} branchBaseNode
|
||||
* @param {Node} leafNode
|
||||
*/
|
||||
constructor(branchBaseNode, leafNode){
|
||||
this.#branchBaseNode = branchBaseNode
|
||||
this.#leafNode = leafNode
|
||||
|
||||
this.#ancestors = getAncestors(this.#leafNode, this.#branchBaseNode).reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} n
|
||||
* @returns {Node | undefined}
|
||||
*/
|
||||
at(n){
|
||||
return this.#ancestors.at(n)
|
||||
}
|
||||
|
||||
removeLeafAndEmptyAncestors(){
|
||||
//this.logBranch('[removeLeafAndEmptyAncestors] branch at the start')
|
||||
|
||||
// it may happen (else marker of if/else/endif) that the leaf was already removed as part of another block
|
||||
// so before removing anything, let's update #ancestors and #leaf
|
||||
|
||||
this.#ancestors.every((ancestor, i) => {
|
||||
if(!ancestor.parentNode){
|
||||
// ancestor already removed from tree
|
||||
this.#ancestors = this.#ancestors.slice(0, i)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true // continue
|
||||
})
|
||||
|
||||
this.#leafNode = this.#ancestors.at(-1)
|
||||
|
||||
//this.logBranch('[removeLeafAndEmptyAncestors] after adjusting this.#ancestors')
|
||||
|
||||
//console.log('removeLeafAndEmptyAncestors', this.#startNode.textContent)
|
||||
let nextLeaf
|
||||
if(this.#leafNode !== this.#branchBaseNode){
|
||||
nextLeaf = this.#leafNode.parentNode
|
||||
//console.log('nextLeaf', !!nextLeaf)
|
||||
nextLeaf.removeChild(this.#leafNode)
|
||||
this.#leafNode = nextLeaf
|
||||
}
|
||||
|
||||
while(this.#leafNode !== this.#branchBaseNode &&
|
||||
(this.#leafNode.textContent === null || this.#leafNode.textContent.trim() === ''))
|
||||
{
|
||||
nextLeaf = this.#leafNode.parentNode
|
||||
this.#leafNode.parentNode.removeChild(this.#leafNode)
|
||||
this.#leafNode = nextLeaf
|
||||
}
|
||||
|
||||
this.#ancestors = getAncestors(this.#leafNode, this.#branchBaseNode).reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} [startIndex]
|
||||
*/
|
||||
removeRightContent(startIndex = 0){
|
||||
//console.log('[removeRightContent]', startIndex, this.#ancestors.slice(startIndex).length)
|
||||
|
||||
for(const branchNode of this.#ancestors.slice(startIndex)){
|
||||
//console.log('[removeRightContent]', branchNode.nodeType, branchNode.nodeName)
|
||||
|
||||
let toRemove = branchNode.nextSibling
|
||||
|
||||
while(toRemove){
|
||||
const toRemoveNext = toRemove.nextSibling
|
||||
toRemove.parentNode.removeChild(toRemove)
|
||||
toRemove = toRemoveNext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} [startIndex]
|
||||
*/
|
||||
removeLeftContent(startIndex = 0){
|
||||
for(const branchNode of this.#ancestors.slice(startIndex)){
|
||||
let toRemove = branchNode.previousSibling
|
||||
|
||||
while(toRemove){
|
||||
const toRemoveNext = toRemove.previousSibling
|
||||
toRemove.parentNode.removeChild(toRemove)
|
||||
toRemove = toRemoveNext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number[]}
|
||||
*/
|
||||
getBranchPath(){
|
||||
//console.log('[getBranchPath]', this.#branchBaseNode.nodeName, this.#branchBaseNode.textContent)
|
||||
//console.log('[getBranchPath] leaf', this.#leafNode.nodeName, this.#leafNode.textContent)
|
||||
|
||||
/** @type {ReturnType<typeof TemplateDOMBranch.prototype.getBranchPath>} */
|
||||
const pathFromLeafToBase = [];
|
||||
|
||||
let currentNode = this.#leafNode
|
||||
let currentNodeParent = currentNode.parentNode
|
||||
|
||||
while(currentNodeParent){
|
||||
//console.log('[getBranchPath] currentNodeParent', currentNodeParent.nodeName)
|
||||
//console.log('[getBranchPath] looking for currentNode', currentNode.nodeName, currentNode.textContent)
|
||||
//console.log('[getBranchPath] currentNodeParent.childNodes.length', currentNodeParent.childNodes.length)
|
||||
/*console.log('[getBranchPath] currentNodeParent.childNodes', Array.from(currentNodeParent.childNodes)
|
||||
.map(n => `${n.nodeName} - ${n.textContent}`)
|
||||
)*/
|
||||
|
||||
const index = Array.from(currentNodeParent.childNodes).indexOf(currentNode)
|
||||
//console.log('[getBranchPath] indexOf', index)
|
||||
if(index === -1){
|
||||
throw new Error(`Could not find currentNode in currentNodeParent's childNodes`)
|
||||
}
|
||||
pathFromLeafToBase.push(index)
|
||||
//console.log('[getBranchPath] currentNodeParent and index', currentNodeParent.nodeName, index)
|
||||
|
||||
if(currentNodeParent === this.#ancestors[0]){
|
||||
break; // path is fnished
|
||||
}
|
||||
else{
|
||||
currentNode = currentNodeParent
|
||||
currentNodeParent = currentNode.parentNode
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-expect-error ES2023
|
||||
return pathFromLeafToBase.toReversed()
|
||||
}
|
||||
|
||||
logBranch(message){
|
||||
console.group('[TemplateDOMBranch] Showing branch')
|
||||
console.log(message)
|
||||
for(const node of this.#ancestors){
|
||||
console.log('branch node', node.nodeType, node.nodeName, node.nodeType === node.TEXT_NODE ? node.textContent : '')
|
||||
}
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
class TemplateBlock{
|
||||
/** @type {Element | Document | DocumentFragment} */
|
||||
#commonAncestor;
|
||||
/** @type {TemplateDOMBranch} */
|
||||
startBranch;
|
||||
/** @type {TemplateDOMBranch} */
|
||||
endBranch;
|
||||
|
||||
/** @type {Node[]} */
|
||||
#middleContent;
|
||||
|
||||
/**@type {any} */
|
||||
#addImageToOdtFile;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Node} startNode
|
||||
* @param {Node} endNode
|
||||
* @param {(OdfjsImage) => string} addImageToOdtFile
|
||||
*/
|
||||
constructor(startNode, endNode, addImageToOdtFile){
|
||||
this.#addImageToOdtFile = addImageToOdtFile
|
||||
|
||||
// @ts-expect-error xmldom.Node
|
||||
this.#commonAncestor = findCommonAncestor(startNode, endNode)
|
||||
|
||||
//console.log('create start branch')
|
||||
this.startBranch = new TemplateDOMBranch(this.#commonAncestor, startNode)
|
||||
//console.log('create end branch')
|
||||
this.endBranch = new TemplateDOMBranch(this.#commonAncestor, endNode)
|
||||
|
||||
|
||||
|
||||
this.#middleContent = []
|
||||
|
||||
let content = this.startBranch.at(1).nextSibling
|
||||
while(content && content !== this.endBranch.at(1)){
|
||||
this.#middleContent.push(content)
|
||||
content = content.nextSibling
|
||||
}
|
||||
|
||||
//console.group('\n== TemplateBlock ==')
|
||||
//this.startBranch.logBranch('startBranch')
|
||||
//console.log('middleContent', this.#middleContent.map(n => n.textContent).join(''))
|
||||
//this.endBranch.logBranch('endBranch')
|
||||
//console.log('common ancestor', this.#commonAncestor.nodeName, '\n')
|
||||
//console.groupEnd()
|
||||
}
|
||||
|
||||
removeMarkersAndEmptyAncestors(){
|
||||
//console.log('[removeMarkersAndEmptyAncestors]', this.#commonAncestor.textContent)
|
||||
this.startBranch.removeLeafAndEmptyAncestors()
|
||||
this.endBranch.removeLeafAndEmptyAncestors()
|
||||
//console.log('[removeMarkersAndEmptyAncestors] after', this.#commonAncestor.textContent)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Compartment} compartement
|
||||
*/
|
||||
fillBlockContentTemplate(compartement){
|
||||
//console.log('[fillBlockContentTemplate] start')
|
||||
|
||||
const startChild = this.startBranch.at(1)
|
||||
if(startChild /*&& startChild !== */){
|
||||
//console.log('[fillBlockContentTemplate] startChild', startChild.nodeName, startChild.textContent)
|
||||
fillOdtElementTemplate(startChild, compartement, this.#addImageToOdtFile)
|
||||
}
|
||||
//console.log('[fillBlockContentTemplate] after startChild')
|
||||
|
||||
|
||||
// if content consists of several parts of an {#each}{/each}
|
||||
// when arriving to the {/each}, it will be alone (and imbalanced)
|
||||
// and will trigger an error
|
||||
fillOdtElementTemplate(Array.from(this.#middleContent), compartement, this.#addImageToOdtFile)
|
||||
|
||||
//console.log('[fillBlockContentTemplate] after middleContent')
|
||||
|
||||
const endChild = this.endBranch.at(1)
|
||||
//console.log('fillBlockContentTemplate] [endBranch]')
|
||||
//this.endBranch.logBranch('endBranch')
|
||||
|
||||
if(endChild){
|
||||
//console.log('[fillBlockContentTemplate] endChild', endChild.nodeName, endChild.textContent)
|
||||
fillOdtElementTemplate(endChild, compartement, this.#addImageToOdtFile)
|
||||
}
|
||||
//console.log('[fillBlockContentTemplate] after endChild')
|
||||
|
||||
//console.log('[fillBlockContentTemplate] end')
|
||||
}
|
||||
|
||||
removeContent(){
|
||||
this.startBranch.removeRightContent(2)
|
||||
|
||||
for(const content of this.#middleContent){
|
||||
content.parentNode.removeChild(content)
|
||||
}
|
||||
|
||||
this.endBranch.removeLeftContent(2)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @returns {TemplateBlock}
|
||||
*/
|
||||
cloneAndAppendAfter(){
|
||||
//console.log('[cloneAndAppendAfter]')
|
||||
const clonedPieces = []
|
||||
|
||||
let startBranchClone;
|
||||
let endBranchClone;
|
||||
|
||||
for(const sibling of [this.startBranch.at(1), ...this.#middleContent, this.endBranch.at(1)]){
|
||||
if(sibling){
|
||||
const siblingClone = sibling.cloneNode(true)
|
||||
clonedPieces.push(siblingClone)
|
||||
|
||||
if(sibling === this.startBranch.at(1))
|
||||
startBranchClone = siblingClone
|
||||
|
||||
if(sibling === this.endBranch.at(1))
|
||||
endBranchClone = siblingClone
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let startChildPreviousSiblingsCount = 0
|
||||
let previousSibling = this.startBranch.at(1).previousSibling
|
||||
while(previousSibling){
|
||||
startChildPreviousSiblingsCount = startChildPreviousSiblingsCount + 1
|
||||
previousSibling = previousSibling.previousSibling
|
||||
}
|
||||
|
||||
const startBranchPathFromBaseToLeaf = this.startBranch.getBranchPath().slice(1)
|
||||
const endBranchPathFromBaseToLeaf = this.endBranch.getBranchPath().slice(1)
|
||||
|
||||
|
||||
//console.log('startBranchClone', !!startBranchClone)
|
||||
//console.log('startBranchPathFromBaseToLeaf', startBranchPathFromBaseToLeaf)
|
||||
|
||||
let startLeafCloneNode
|
||||
{
|
||||
let node = startBranchClone
|
||||
for(let pathIndex of startBranchPathFromBaseToLeaf){
|
||||
//console.log('[startLeafCloneNode] node.childNodes.length', node.childNodes.length)
|
||||
//console.log('[startLeafCloneNode] pathIndex', pathIndex)
|
||||
|
||||
node = node.childNodes[pathIndex]
|
||||
}
|
||||
startLeafCloneNode = node
|
||||
}
|
||||
|
||||
//console.log('endBranchClone', !!endBranchClone)
|
||||
//console.log('endBranchPathFromBaseToLeaf', endBranchPathFromBaseToLeaf)
|
||||
|
||||
let endLeafCloneNode
|
||||
{
|
||||
let node = endBranchClone
|
||||
for(let pathIndex of endBranchPathFromBaseToLeaf){
|
||||
|
||||
//console.log('[endLeafCloneNode] node.childNodes.length', node.childNodes.length)
|
||||
//console.log('[endLeafCloneNode] pathIndex', pathIndex)
|
||||
|
||||
node = node.childNodes[pathIndex]
|
||||
}
|
||||
endLeafCloneNode = node
|
||||
}
|
||||
|
||||
let insertBeforePoint = this.endBranch.at(1) && this.endBranch.at(1).nextSibling
|
||||
|
||||
if(insertBeforePoint){
|
||||
for(const node of clonedPieces){
|
||||
this.#commonAncestor.insertBefore(node, insertBeforePoint)
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(const node of clonedPieces){
|
||||
this.#commonAncestor.appendChild(node)
|
||||
}
|
||||
}
|
||||
|
||||
return new TemplateBlock(startLeafCloneNode, endLeafCloneNode, this.#addImageToOdtFile)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {Compartment} compartment
|
||||
* @returns {TextPlaceToFill | undefined}
|
||||
*/
|
||||
function findPlacesToFillInString(str, compartment) {
|
||||
const varRexExp = new RegExp(variableRegex.source, 'g');
|
||||
const matches = str.matchAll(varRexExp)
|
||||
|
||||
/** @type {TextPlaceToFill['expressions']} */
|
||||
const expressions = []
|
||||
|
||||
/** @type {(string | ((data:any) => void))[]} */
|
||||
const parts = []
|
||||
let remaining = str;
|
||||
|
||||
for(const match of matches) {
|
||||
//console.log('match', match)
|
||||
const [matched, group1] = match
|
||||
|
||||
const replacedString = matched
|
||||
const expression = group1.trim()
|
||||
expressions.push({expression, replacedString})
|
||||
|
||||
const [fixedPart, newRemaining] = remaining.split(replacedString, 2)
|
||||
|
||||
if(fixedPart.length >= 1)
|
||||
parts.push(fixedPart)
|
||||
|
||||
parts.push(() => compartment.evaluate(expression))
|
||||
|
||||
remaining = newRemaining
|
||||
}
|
||||
|
||||
if(remaining.length >= 1)
|
||||
parts.push(remaining)
|
||||
|
||||
//console.log('parts', parts)
|
||||
|
||||
|
||||
if(remaining === str) {
|
||||
// no match found
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
return {
|
||||
expressions,
|
||||
fill: (data) => {
|
||||
return parts.map(p => {
|
||||
if(typeof p === 'string')
|
||||
return p
|
||||
else
|
||||
return p(data)
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Node} ifOpeningMarkerNode
|
||||
* @param {Node | undefined} ifElseMarkerNode
|
||||
* @param {Node} ifClosingMarkerNode
|
||||
* @param {string} ifBlockConditionExpression
|
||||
* @param {Compartment} compartment
|
||||
* // TODO type,addImageToOdtFile
|
||||
*/
|
||||
function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment, addImageToOdtFile) {
|
||||
//const docEl = ifOpeningMarkerNode.ownerDocument.documentElement
|
||||
|
||||
const conditionValue = compartment.evaluate(ifBlockConditionExpression)
|
||||
|
||||
/** @type {TemplateBlock | undefined} */
|
||||
let thenTemplateBlock
|
||||
/** @type {TemplateBlock | undefined} */
|
||||
let elseTemplateBlock
|
||||
|
||||
if(ifElseMarkerNode) {
|
||||
/*console.log('before first extract',
|
||||
ifOpeningMarkerNode.childNodes.length, ifOpeningMarkerNode.textContent,
|
||||
ifElseMarkerNode.childNodes.length, ifElseMarkerNode.textContent
|
||||
)*/
|
||||
|
||||
thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifElseMarkerNode, addImageToOdtFile)
|
||||
elseTemplateBlock = new TemplateBlock(ifElseMarkerNode, ifClosingMarkerNode, addImageToOdtFile)
|
||||
}
|
||||
else {
|
||||
thenTemplateBlock = new TemplateBlock(ifOpeningMarkerNode, ifClosingMarkerNode, addImageToOdtFile)
|
||||
}
|
||||
|
||||
if(conditionValue) {
|
||||
if(elseTemplateBlock){
|
||||
elseTemplateBlock.removeContent()
|
||||
}
|
||||
|
||||
thenTemplateBlock.removeMarkersAndEmptyAncestors()
|
||||
if(elseTemplateBlock){
|
||||
elseTemplateBlock.removeMarkersAndEmptyAncestors()
|
||||
}
|
||||
|
||||
thenTemplateBlock.fillBlockContentTemplate(compartment)
|
||||
}
|
||||
else{
|
||||
// remove content before removing markers so that right and left content are fully removed
|
||||
thenTemplateBlock.removeContent()
|
||||
|
||||
thenTemplateBlock.removeMarkersAndEmptyAncestors()
|
||||
if(elseTemplateBlock){
|
||||
elseTemplateBlock.removeMarkersAndEmptyAncestors()
|
||||
}
|
||||
|
||||
if(elseTemplateBlock){
|
||||
elseTemplateBlock.fillBlockContentTemplate(compartment)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Node} startNode
|
||||
* @param {string} iterableExpression
|
||||
* @param {string} itemExpression
|
||||
* @param {Node} endNode
|
||||
* @param {Compartment} compartment
|
||||
* // TODO type addImageToOdtFile
|
||||
*/
|
||||
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment, addImageToOdtFile) {
|
||||
//console.log('fillEachBlock', iterableExpression, itemExpression)
|
||||
|
||||
const docEl = startNode.ownerDocument.documentElement
|
||||
//console.log('[fillEachBlock] docEl', docEl.textContent)
|
||||
|
||||
const repeatedTemplateBlock = new TemplateBlock(startNode, endNode, addImageToOdtFile)
|
||||
|
||||
// Find the iterable in the data
|
||||
let iterable = compartment.evaluate(iterableExpression)
|
||||
if(!iterable || typeof iterable[Symbol.iterator] !== 'function') {
|
||||
// when there is no iterable, silently replace with empty array
|
||||
iterable = []
|
||||
}
|
||||
|
||||
// convert to array to know the size and know which element is last
|
||||
if(!Array.isArray(iterable))
|
||||
iterable = [...iterable]
|
||||
|
||||
|
||||
if(iterable.length === 0){
|
||||
repeatedTemplateBlock.removeMarkersAndEmptyAncestors()
|
||||
repeatedTemplateBlock.removeContent()
|
||||
}
|
||||
else{
|
||||
let nextTemplateBlock = repeatedTemplateBlock
|
||||
|
||||
iterable.forEach((item, i) => {
|
||||
//console.log('[fillEachBlock] loop i', i, docEl.textContent)
|
||||
const firstItem = i === 0
|
||||
const lastItem = i === iterable.length - 1
|
||||
let currentTemplateBlock = nextTemplateBlock;
|
||||
|
||||
//console.log('currentTemplateBlock', currentTemplateBlock.startBranch.at(0).textContent)
|
||||
|
||||
if(!lastItem){
|
||||
nextTemplateBlock = currentTemplateBlock.cloneAndAppendAfter()
|
||||
}
|
||||
|
||||
let insideCompartment = new Compartment({
|
||||
globals: Object.assign({}, compartment.globalThis, {[itemExpression]: item}),
|
||||
__options__: true
|
||||
})
|
||||
|
||||
if(!firstItem){
|
||||
currentTemplateBlock.startBranch.removeLeftContent(2)
|
||||
}
|
||||
if(!lastItem){
|
||||
//console.log('[fillEachBlock] removeRightContent')
|
||||
currentTemplateBlock.endBranch.removeRightContent(2)
|
||||
}
|
||||
|
||||
//console.log('[fillEachBlock] docEl i before removeMarkers', i, docEl.textContent)
|
||||
currentTemplateBlock.removeMarkersAndEmptyAncestors()
|
||||
//console.log('[fillEachBlock] docEl i after removeMarkers', i, docEl.textContent)
|
||||
|
||||
//console.log('\nrecursive call to fillBlockContentTemplate')
|
||||
currentTemplateBlock.fillBlockContentTemplate(insideCompartment)
|
||||
|
||||
//console.log('[fillEachBlock] docEl i after remove contents', i, docEl.textContent)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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 EACH = eachStartMarkerRegex.source
|
||||
|
||||
/** @typedef {Element | DocumentFragment | Document} RootElementArgument */
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RootElementArgument | RootElementArgument[]} rootElements
|
||||
* @param {Compartment} compartment
|
||||
* @param {(OdfjsImage) => string} addImageToOdtFile
|
||||
* @returns {void}
|
||||
*/
|
||||
export default function fillOdtElementTemplate(rootElements, compartment, addImageToOdtFile) {
|
||||
|
||||
if(!Array.isArray(rootElements)){
|
||||
rootElements = [rootElements]
|
||||
}
|
||||
|
||||
//console.log('[fillTemplatedOdtElement]', rootElements.length, rootElements[0].nodeType, rootElements[0].nodeName, rootElements[0].textContent)
|
||||
//console.log('[fillTemplatedOdtElement]', rootElement.documentElement && rootElement.documentElement.textContent)
|
||||
|
||||
let currentlyOpenBlocks = []
|
||||
|
||||
/** @type {Node | undefined} */
|
||||
let eachOpeningMarkerNode
|
||||
/** @type {Node | undefined} */
|
||||
let eachClosingMarkerNode
|
||||
|
||||
let eachBlockIterableExpression, eachBlockItemExpression;
|
||||
|
||||
|
||||
/** @type {Node | undefined} */
|
||||
let ifOpeningMarkerNode
|
||||
/** @type {Node | undefined} */
|
||||
let ifElseMarkerNode
|
||||
/** @type {Node | undefined} */
|
||||
let ifClosingMarkerNode
|
||||
|
||||
let ifBlockConditionExpression
|
||||
// Traverse "in document order"
|
||||
|
||||
|
||||
for(const rootElement of rootElements){
|
||||
|
||||
// @ts-ignore
|
||||
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 || ''
|
||||
|
||||
/**
|
||||
* looking for {#each x as y}
|
||||
*/
|
||||
const eachStartMatch = text.match(eachStartMarkerRegex);
|
||||
|
||||
if(eachStartMatch) {
|
||||
//console.log('startMatch', startMatch)
|
||||
|
||||
currentlyOpenBlocks.push(EACH)
|
||||
|
||||
if(insideAnOpenBlock) {
|
||||
// do nothing
|
||||
}
|
||||
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, currentlyOpenBlocks)
|
||||
|
||||
if(!insideAnOpenBlock)
|
||||
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
|
||||
//console.log('start of fillEachBlock')
|
||||
|
||||
fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment, addImageToOdtFile)
|
||||
|
||||
//console.log('end of fillEachBlock')
|
||||
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
import {ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayReader, TextWriter, Uint8ArrayWriter} from '@zip.js/zip.js';
|
||||
|
||||
import {parseXML, serializeToString} from '../../DOMUtils.js'
|
||||
import {makeManifestFile, getManifestFileData} from '../manifest.js';
|
||||
import prepareTemplateDOMTree from './prepareTemplateDOMTree.js';
|
||||
|
||||
import 'ses'
|
||||
import fillOdtElementTemplate from './fillOdtElementTemplate.js';
|
||||
|
||||
|
||||
/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */
|
||||
/** @import {ODFManifest, ODFManifestFileEntry} from '../manifest.js' */
|
||||
/** @import {OdfjsImage} from '../../types.js' */
|
||||
|
||||
/** @typedef {ArrayBuffer} ODTFile */
|
||||
|
||||
const ODTMimetype = 'application/vnd.oasis.opendocument.text'
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Document} document
|
||||
* @param {Compartment} compartment
|
||||
* @param {(OdfjsImage) => string} addImageToOdtFile
|
||||
* @returns {void}
|
||||
*/
|
||||
function fillOdtDocumentTemplate(document, compartment, addImageToOdtFile) {
|
||||
prepareTemplateDOMTree(document)
|
||||
fillOdtElementTemplate(document, compartment, addImageToOdtFile)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const keptFiles = new Set(['content.xml', 'styles.xml', 'mimetype', 'META-INF/manifest.xml'])
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filename
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function keepFile(filename) {
|
||||
return keptFiles.has(filename) || filename.startsWith('Pictures/')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {ODTFile} odtTemplate
|
||||
* @param {any} data
|
||||
* @returns {Promise<ODTFile>}
|
||||
*/
|
||||
export default async function fillOdtTemplate(odtTemplate, data) {
|
||||
|
||||
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtTemplate)));
|
||||
|
||||
// Lire toutes les entrées du fichier ODT
|
||||
const entries = reader.getEntriesGenerator();
|
||||
|
||||
// Créer un ZipWriter pour le nouveau fichier ODT
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
|
||||
/** @type {ODFManifest} */
|
||||
let manifestFileData;
|
||||
|
||||
/** @type {{filename: string, content: Reader, options?: ZipWriterAddDataOptions}[]} */
|
||||
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
|
||||
for await(const entry of entries) {
|
||||
const filename = entry.filename
|
||||
|
||||
//console.log('entry', filename, entry.directory)
|
||||
|
||||
// remove other files
|
||||
if(!keepFile(filename)) {
|
||||
// ignore, do not create a corresponding entry in the new zip
|
||||
}
|
||||
else {
|
||||
let content
|
||||
let options
|
||||
|
||||
switch(filename) {
|
||||
case 'mimetype':
|
||||
content = new TextReader(ODTMimetype)
|
||||
options = {
|
||||
level: 0,
|
||||
compressionMethod: 0,
|
||||
dataDescriptor: false,
|
||||
extendedTimestamp: false,
|
||||
}
|
||||
|
||||
zipEntriesToAdd.push({filename, content, options})
|
||||
|
||||
break;
|
||||
case 'content.xml':
|
||||
// @ts-ignore
|
||||
const contentXml = await entry.getData(new TextWriter());
|
||||
const contentDocument = parseXML(contentXml);
|
||||
|
||||
|
||||
|
||||
const compartment = new Compartment({
|
||||
globals: data,
|
||||
__options__: true
|
||||
})
|
||||
|
||||
fillOdtDocumentTemplate(contentDocument, compartment, addImageToOdtFile)
|
||||
|
||||
const updatedContentXml = serializeToString(contentDocument)
|
||||
|
||||
content = new TextReader(updatedContentXml)
|
||||
options = {
|
||||
lastModDate: entry.lastModDate,
|
||||
level: 9
|
||||
};
|
||||
|
||||
zipEntriesToAdd.push({filename, content, options})
|
||||
|
||||
break;
|
||||
|
||||
case 'META-INF/manifest.xml':
|
||||
// @ts-ignore
|
||||
const manifestXml = await entry.getData(new TextWriter());
|
||||
const manifestDocument = parseXML(manifestXml);
|
||||
manifestFileData = getManifestFileData(manifestDocument)
|
||||
|
||||
break;
|
||||
|
||||
case 'styles.xml':
|
||||
default:
|
||||
const blobWriter = new BlobWriter();
|
||||
// @ts-ignore
|
||||
await entry.getData(blobWriter);
|
||||
const blob = await blobWriter.getData();
|
||||
|
||||
content = new BlobReader(blob)
|
||||
zipEntriesToAdd.push({filename, content})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(const {fullPath, mediaType} of newManifestEntries){
|
||||
manifestFileData.fileEntries.set(fullPath, {fullPath, mediaType})
|
||||
}
|
||||
|
||||
for(const {filename, content, options} of zipEntriesToAdd) {
|
||||
await writer.add(filename, content, options);
|
||||
}
|
||||
|
||||
const newZipFilenames = new Set(zipEntriesToAdd.map(ze => ze.filename))
|
||||
|
||||
if(!manifestFileData) {
|
||||
throw new Error(`'META-INF/manifest.xml' zip entry missing`)
|
||||
}
|
||||
|
||||
// remove ignored files from manifest.xml
|
||||
for(const filename of manifestFileData.fileEntries.keys()) {
|
||||
if(!newZipFilenames.has(filename)) {
|
||||
manifestFileData.fileEntries.delete(filename)
|
||||
}
|
||||
}
|
||||
|
||||
const manifestFileXml = makeManifestFile(manifestFileData)
|
||||
await writer.add('META-INF/manifest.xml', new TextReader(manifestFileXml));
|
||||
|
||||
await reader.close();
|
||||
|
||||
return writer.close();
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
// the regexps below are shared, so they shoudn't have state (no 'g' flag)
|
||||
export const variableRegex = /\{([^{#\/:]+?)\}/
|
||||
export const imageMarkerRegex = /{#image\s+([^}]+?)\s*}/;
|
||||
|
||||
export const ifStartMarkerRegex = /{#if\s+([^}]+?)\s*}/;
|
||||
export const elseMarker = '{:else}'
|
||||
export const closingIfMarker = '{/if}'
|
||||
|
||||
export const eachStartMarkerRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/;
|
||||
export const eachClosingMarker = '{/each}'
|
||||
@ -1,530 +0,0 @@
|
||||
//@ts-check
|
||||
|
||||
import {traverse, Node, getAncestors, findCommonAncestor} from "../../DOMUtils.js";
|
||||
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js'
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {string | RegExp} pattern
|
||||
* @returns {{marker: string, index: number}[]}
|
||||
*/
|
||||
function findAllMatches(text, pattern) {
|
||||
const results = [];
|
||||
let match;
|
||||
|
||||
if(typeof pattern === 'string') {
|
||||
// For string markers like elseMarker and closingIfMarker
|
||||
let index = 0;
|
||||
while((index = text.indexOf(pattern, index)) !== -1) {
|
||||
results.push({
|
||||
marker: pattern,
|
||||
index: index
|
||||
});
|
||||
index += pattern.length;
|
||||
}
|
||||
} else {
|
||||
// For regex patterns
|
||||
pattern = new RegExp(pattern.source, 'g');
|
||||
while((match = pattern.exec(text)) !== null) {
|
||||
results.push({
|
||||
marker: match[0],
|
||||
index: match.index
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* text position of a node relative to a text nodes within a container
|
||||
*
|
||||
* @param {Text} node
|
||||
* @param {Text[]} containerTextNodes
|
||||
* @returns {number}
|
||||
*/
|
||||
function getNodeTextPosition(node, containerTextNodes) {
|
||||
let position = 0;
|
||||
|
||||
for(const currentTextNode of containerTextNodes) {
|
||||
if(currentTextNode === node) {
|
||||
return position
|
||||
}
|
||||
else {
|
||||
position += (currentTextNode.textContent || '').length;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`[${getNodeTextPosition.name}] None of containerTextNodes elements is equal to node`)
|
||||
}
|
||||
|
||||
|
||||
/** @typedef {Node[]} DOMPath */
|
||||
|
||||
/**
|
||||
* remove nodes between startNode and endNode
|
||||
* including startNode and endNode
|
||||
*
|
||||
* @param {Node} startNode
|
||||
* @param {Node} endNode
|
||||
* @param {string} text
|
||||
* @returns {void}
|
||||
*/
|
||||
function replaceBetweenNodesWithText(startNode, endNode, text) {
|
||||
// find both ancestry branch
|
||||
const startNodeAncestors = new Set(getAncestors(startNode))
|
||||
const endNodeAncestors = new Set(getAncestors(endNode))
|
||||
|
||||
// find common ancestor
|
||||
const commonAncestor = findCommonAncestor(startNode, endNode)
|
||||
|
||||
let remove = false
|
||||
let toRemove = []
|
||||
let commonAncestorChild = commonAncestor.firstChild
|
||||
let commonAncestorInsertionChild
|
||||
|
||||
while(commonAncestorChild){
|
||||
if(startNodeAncestors.has(commonAncestorChild)){
|
||||
remove = true
|
||||
}
|
||||
|
||||
if(remove){
|
||||
toRemove.push(commonAncestorChild)
|
||||
|
||||
if(endNodeAncestors.has(commonAncestorChild)){
|
||||
commonAncestorInsertionChild = commonAncestorChild.nextSibling
|
||||
break;
|
||||
}
|
||||
}
|
||||
commonAncestorChild = commonAncestorChild.nextSibling
|
||||
}
|
||||
|
||||
for(const node of toRemove){
|
||||
commonAncestor.removeChild(node)
|
||||
}
|
||||
|
||||
//console.log('replaceBetweenNodesWithText startNode', startNode.textContent)
|
||||
|
||||
const newTextNode = commonAncestor.ownerDocument.createTextNode(text)
|
||||
|
||||
if(commonAncestorInsertionChild){
|
||||
commonAncestor.insertBefore(newTextNode, commonAncestorInsertionChild)
|
||||
}
|
||||
else{
|
||||
commonAncestor.appendChild(newTextNode)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidate markers which are split among several Text nodes
|
||||
*
|
||||
* @param {Document} document
|
||||
*/
|
||||
function consolidateMarkers(document){
|
||||
// Perform a first pass to detect templating markers with formatting to remove it
|
||||
const potentialMarkersContainers = [
|
||||
...Array.from(document.getElementsByTagName('text:p')),
|
||||
...Array.from(document.getElementsByTagName('text:h'))
|
||||
]
|
||||
|
||||
for(const potentialMarkersContainer of potentialMarkersContainers) {
|
||||
/** @type {{marker: string, index: number}[]} */
|
||||
const consolidatedMarkers = []
|
||||
|
||||
/** @type {Text[]} */
|
||||
let containerTextNodesInTreeOrder = [];
|
||||
|
||||
function refreshContainerTextNodes(){
|
||||
containerTextNodesInTreeOrder = []
|
||||
|
||||
traverse(potentialMarkersContainer, node => {
|
||||
if(node.nodeType === Node.TEXT_NODE) {
|
||||
containerTextNodesInTreeOrder.push(/** @type {Text} */(node))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
refreshContainerTextNodes()
|
||||
|
||||
let fullText = ''
|
||||
for(const node of containerTextNodesInTreeOrder){
|
||||
fullText = fullText + node.textContent
|
||||
}
|
||||
|
||||
// Check for each template marker
|
||||
const positionedMarkers = [
|
||||
...findAllMatches(fullText, ifStartMarkerRegex),
|
||||
...findAllMatches(fullText, elseMarker),
|
||||
...findAllMatches(fullText, closingIfMarker),
|
||||
...findAllMatches(fullText, eachStartMarkerRegex),
|
||||
...findAllMatches(fullText, eachClosingMarker),
|
||||
...findAllMatches(fullText, variableRegex)
|
||||
];
|
||||
|
||||
|
||||
//if(positionedMarkers.length >= 1)
|
||||
// console.log('positionedMarkers', positionedMarkers)
|
||||
|
||||
|
||||
while(consolidatedMarkers.length < positionedMarkers.length) {
|
||||
refreshContainerTextNodes()
|
||||
|
||||
// For each marker, check if it's contained within a single text node
|
||||
for(const positionedMarker of positionedMarkers.slice(consolidatedMarkers.length)) {
|
||||
//console.log('positionedMarker', positionedMarker)
|
||||
|
||||
let currentPos = 0;
|
||||
let startNode;
|
||||
let endNode;
|
||||
|
||||
// Find which text node(s) contain this marker
|
||||
for(const textNode of containerTextNodesInTreeOrder) {
|
||||
const nodeStart = currentPos;
|
||||
const nodeEnd = nodeStart + textNode.textContent.length;
|
||||
|
||||
// If start of marker is in this node
|
||||
if(!startNode && positionedMarker.index >= nodeStart && positionedMarker.index < nodeEnd) {
|
||||
startNode = textNode;
|
||||
}
|
||||
|
||||
// If end of marker is in this node
|
||||
if(startNode && positionedMarker.index + positionedMarker.marker.length > nodeStart &&
|
||||
positionedMarker.index + positionedMarker.marker.length <= nodeEnd) {
|
||||
endNode = textNode;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPos = nodeEnd;
|
||||
}
|
||||
|
||||
if(!startNode){
|
||||
throw new Error(`Could not find startNode for marker '${positionedMarker.marker}'`)
|
||||
}
|
||||
|
||||
if(!endNode){
|
||||
throw new Error(`Could not find endNode for marker '${positionedMarker.marker}'`)
|
||||
}
|
||||
|
||||
// Check if marker spans multiple nodes
|
||||
if(startNode !== endNode) {
|
||||
//console.log('startNode !== endNode', startNode.textContent, endNode.textContent)
|
||||
const commonAncestor = findCommonAncestor(startNode, endNode)
|
||||
|
||||
/** @type {Node} */
|
||||
let commonAncestorStartChild = startNode
|
||||
while(commonAncestorStartChild.parentNode !== commonAncestor){
|
||||
commonAncestorStartChild = commonAncestorStartChild.parentNode
|
||||
}
|
||||
|
||||
/** @type {Node} */
|
||||
let commonAncestorEndChild = endNode
|
||||
while(commonAncestorEndChild.parentNode !== commonAncestor){
|
||||
commonAncestorEndChild = commonAncestorEndChild.parentNode
|
||||
}
|
||||
|
||||
// Calculate relative positions within the nodes
|
||||
let startNodeTextContent = startNode.textContent || '';
|
||||
let endNodeTextContent = endNode.textContent || '';
|
||||
|
||||
// Calculate the position within the start node
|
||||
let posInStartNode = positionedMarker.index - getNodeTextPosition(startNode, containerTextNodesInTreeOrder);
|
||||
|
||||
// Calculate the position within the end node
|
||||
let posInEndNode = (positionedMarker.index + positionedMarker.marker.length) - getNodeTextPosition(endNode, containerTextNodesInTreeOrder);
|
||||
|
||||
let newStartNode = startNode
|
||||
|
||||
// if there is before-text, split
|
||||
if(posInStartNode > 0) {
|
||||
// Text exists before the marker - preserve it
|
||||
|
||||
// set newStartNode to a Text node containing only the marker beginning
|
||||
newStartNode = startNode.splitText(posInStartNode)
|
||||
// startNode/beforeStartNode now contains only non-marker text
|
||||
|
||||
// then, by definition of .splitText(posInStartNode):
|
||||
posInStartNode = 0
|
||||
|
||||
// move the marker beginning part to become a child of commonAncestor
|
||||
newStartNode.parentNode?.removeChild(newStartNode)
|
||||
|
||||
commonAncestor.insertBefore(newStartNode, commonAncestorStartChild.nextSibling)
|
||||
|
||||
//console.log('commonAncestor after before-text split', commonAncestor.textContent )
|
||||
}
|
||||
|
||||
|
||||
// if there is after-text, split
|
||||
if(posInEndNode < endNodeTextContent.length) {
|
||||
// Text exists after the marker - preserve it
|
||||
|
||||
endNode.splitText(posInEndNode);
|
||||
// endNode now contains only the end of marker text
|
||||
|
||||
// then, by definition of .splitText(posInEndNode):
|
||||
posInEndNode = endNodeTextContent.length
|
||||
|
||||
// move the marker ending part to become a child of commonAncestor
|
||||
if(endNode !== commonAncestorEndChild){
|
||||
endNode.parentNode?.removeChild(endNode)
|
||||
commonAncestor.insertBefore(endNode, commonAncestorEndChild)
|
||||
}
|
||||
|
||||
//console.log('commonAncestor after after-text split', commonAncestor.textContent )
|
||||
}
|
||||
|
||||
// then, replace all nodes between (new)startNode and (new)endNode with a single textNode in commonAncestor
|
||||
replaceBetweenNodesWithText(newStartNode, endNode, positionedMarker.marker)
|
||||
|
||||
//console.log('commonAncestor after replaceBetweenNodesWithText', commonAncestor.textContent )
|
||||
|
||||
// After consolidation, break as the DOM structure has changed
|
||||
// and containerTextNodesInTreeOrder needs to be refreshed
|
||||
consolidatedMarkers.push(positionedMarker)
|
||||
break;
|
||||
}
|
||||
|
||||
consolidatedMarkers.push(positionedMarker)
|
||||
}
|
||||
}
|
||||
|
||||
//console.log('consolidatedMarkers', consolidatedMarkers)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {typeof closingIfMarker | typeof eachClosingMarker | typeof eachStartMarkerRegex.source | typeof elseMarker | typeof ifStartMarkerRegex.source | typeof variableRegex.source} MarkerType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MarkerNode
|
||||
* @prop {Node} node
|
||||
* @prop {MarkerType} markerType
|
||||
*/
|
||||
|
||||
/**
|
||||
* isolate markers which are in Text nodes with other texts
|
||||
*
|
||||
* @param {Document} document
|
||||
* @returns {Map<Node, MarkerType>}
|
||||
*/
|
||||
function isolateMarkerText(document){
|
||||
/** @type {ReturnType<isolateMarkerText>} */
|
||||
const markerNodes = new Map()
|
||||
|
||||
traverse(document, currentNode => {
|
||||
//console.log('isolateMarkers', currentNode.nodeName, currentNode.textContent)
|
||||
|
||||
if(currentNode.nodeType === Node.TEXT_NODE) {
|
||||
// find all marker starts and ends and split textNode
|
||||
let remainingText = currentNode.textContent || ''
|
||||
|
||||
while(remainingText.length >= 1) {
|
||||
let matchText;
|
||||
let matchIndex;
|
||||
/** @type {MarkerType} */
|
||||
let markerType;
|
||||
|
||||
// looking for a block marker
|
||||
for(const marker of [ifStartMarkerRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingMarker]) {
|
||||
if(typeof marker === 'string') {
|
||||
const index = remainingText.indexOf(marker)
|
||||
|
||||
if(index !== -1) {
|
||||
matchText = marker
|
||||
matchIndex = index
|
||||
markerType = marker
|
||||
|
||||
// found the first match
|
||||
break; // get out of loop
|
||||
}
|
||||
}
|
||||
else {
|
||||
// marker is a RegExp
|
||||
const match = remainingText.match(marker)
|
||||
|
||||
if(match) {
|
||||
matchText = match[0]
|
||||
matchIndex = match.index
|
||||
markerType = marker.source
|
||||
|
||||
// found the first match
|
||||
break; // get out of loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(matchText) {
|
||||
// split 3-way : before-match, match and after-match
|
||||
|
||||
if(matchText.length < remainingText.length) {
|
||||
// @ts-ignore
|
||||
let afterMatchTextNode = currentNode.splitText(matchIndex + matchText.length)
|
||||
if(afterMatchTextNode.textContent && afterMatchTextNode.textContent.length >= 1) {
|
||||
remainingText = afterMatchTextNode.textContent
|
||||
}
|
||||
else {
|
||||
remainingText = ''
|
||||
}
|
||||
|
||||
// per spec, currentNode now contains before-match and match text
|
||||
|
||||
/** @type {Node} */
|
||||
let matchTextNode
|
||||
|
||||
// @ts-ignore
|
||||
if(matchIndex > 0) {
|
||||
// @ts-ignore
|
||||
matchTextNode = currentNode.splitText(matchIndex)
|
||||
}
|
||||
else{
|
||||
matchTextNode = currentNode
|
||||
}
|
||||
|
||||
markerNodes.set(matchTextNode, markerType)
|
||||
|
||||
// per spec, currentNode now contains only before-match text
|
||||
|
||||
if(afterMatchTextNode) {
|
||||
currentNode = afterMatchTextNode
|
||||
}
|
||||
}
|
||||
else {
|
||||
remainingText = ''
|
||||
}
|
||||
}
|
||||
else {
|
||||
remainingText = ''
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// skip
|
||||
}
|
||||
})
|
||||
|
||||
//console.log('markerNodes', [...markerNodes].map(([node, markerType]) => [node.textContent, markerType]))
|
||||
|
||||
return markerNodes
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* after isolateMatchingMarkersStructure, matching markers (opening/closing each, if/then/closing if)
|
||||
* are put in isolated branches within their common ancestors
|
||||
*
|
||||
* UNFINISHED - maybe another day if relevant
|
||||
*
|
||||
* @param {Document} document
|
||||
* @param {Map<Node, MarkerType>} markerNodes
|
||||
*/
|
||||
//function isolateMatchingMarkersStructure(document, markerNodes){
|
||||
/** @type {MarkerNode[]} */
|
||||
/* let currentlyOpenBlocks = []
|
||||
|
||||
traverse(document, currentNode => {
|
||||
|
||||
const markerType = markerNodes.get(currentNode)
|
||||
|
||||
if(markerType){
|
||||
switch(markerType){
|
||||
case eachStartMarkerRegex.source:
|
||||
case ifStartMarkerRegex.source: {
|
||||
currentlyOpenBlocks.push({
|
||||
node: currentNode,
|
||||
markerType
|
||||
})
|
||||
break;
|
||||
}
|
||||
case eachClosingMarker: {
|
||||
const lastOpenedBlockMarkerNode = currentlyOpenBlocks.pop()
|
||||
|
||||
if(!lastOpenedBlockMarkerNode)
|
||||
throw new Error(`{/each} found without corresponding opening {#each x as y}`)
|
||||
|
||||
if(lastOpenedBlockMarkerNode.markerType !== eachStartMarkerRegex.source)
|
||||
throw new Error(`{/each} found while the last opened block was not an opening {#each x as y} (it was a ${lastOpenedBlockMarkerNode.markerType})`)
|
||||
|
||||
const openingEachNode = lastOpenedBlockMarkerNode.node
|
||||
const closingEachNode = currentNode
|
||||
|
||||
const commonAncestor = findCommonAncestor(openingEachNode, closingEachNode)
|
||||
|
||||
if(openingEachNode.parentNode !== commonAncestor && openingEachNode.parentNode.childNodes.length >= 2){
|
||||
if(openingEachNode.previousSibling){
|
||||
// create branch for previousSiblings
|
||||
let previousSibling = openingEachNode.previousSibling
|
||||
const previousSiblings = []
|
||||
while(previousSibling){
|
||||
previousSiblings.push(previousSibling.previousSibling)
|
||||
previousSibling = previousSibling.previousSibling
|
||||
}
|
||||
|
||||
// put previous siblings in tree order
|
||||
previousSiblings.reverse()
|
||||
|
||||
const parent = openingEachNode.parentNode
|
||||
const parentClone = parent.cloneNode(false)
|
||||
for(const previousSibling of previousSiblings){
|
||||
previousSibling.parentNode.removeChild(previousSibling)
|
||||
parentClone.appendChild(previousSibling)
|
||||
}
|
||||
|
||||
let openingEachNodeBranch = openingEachNode.parentNode
|
||||
let branchForPreviousSiblings = parentClone
|
||||
|
||||
while(openingEachNodeBranch.parentNode !== commonAncestor){
|
||||
const newParentClone = openingEachNodeBranch.parentNode.cloneNode(false)
|
||||
branchForPreviousSiblings.parentNode.removeChild(branchForPreviousSiblings)
|
||||
newParentClone.appendChild(branchForPreviousSiblings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(`MarkerType not recognized: '${markerType}`)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}*/
|
||||
|
||||
|
||||
/**
|
||||
* This function prepares the template DOM tree in a way that makes it easily processed by the template execution
|
||||
* Specifically, after the call to this function, the document is altered to respect the following property:
|
||||
*
|
||||
* each template marker ({#each ... as ...}, {/if}, etc.) placed within a single Text node
|
||||
*
|
||||
* If the template marker was partially formatted in the original document, the formatting is removed so the
|
||||
* marker can be within a single Text node
|
||||
*
|
||||
* If the template marker was in a Text node with other text, the Text node is split in a way to isolate the marker
|
||||
* from the rest of the text
|
||||
*
|
||||
* @param {Document} document
|
||||
*/
|
||||
export default function prepareTemplateDOMTree(document){
|
||||
consolidateMarkers(document)
|
||||
// after consolidateMarkers, each marker is in at most one text node
|
||||
// (formatting with markers is removed)
|
||||
|
||||
isolateMarkerText(document)
|
||||
// after isolateMarkerText, each marker is in exactly one text node
|
||||
// (markers are separated from text that was before or after in the same text node)
|
||||
}
|
||||
@ -1,23 +1,20 @@
|
||||
//@ts-check
|
||||
import { Uint8ArrayReader, ZipReader, TextWriter } from '@zip.js/zip.js';
|
||||
|
||||
import {parseXML} from './DOMUtils.js'
|
||||
|
||||
/** @import {Entry} from '@zip.js/zip.js'*/
|
||||
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent, OdfjsImage} from './types.js' */
|
||||
import { unzip } from 'unzipit';
|
||||
|
||||
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent} from './types.js' */
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-node
|
||||
const TEXT_NODE = 3
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} cell
|
||||
*
|
||||
* @param {Element} cell
|
||||
* @returns {string}
|
||||
*/
|
||||
function extraxtODSCellText(cell) {
|
||||
let text = '';
|
||||
const childNodes = cell.childNodes;
|
||||
|
||||
for (const child of Array.from(childNodes)) {
|
||||
if (child.nodeType === TEXT_NODE) {
|
||||
// Direct text node, append the text directly
|
||||
@ -33,15 +30,13 @@ function extraxtODSCellText(cell) {
|
||||
text += pChild.nodeValue; // Append text inside <text:p>
|
||||
} else if (pChild.nodeName === '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') {
|
||||
text += '\n'; // Append newline for <text:line-break /> directly under <table:table-cell>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
@ -49,33 +44,15 @@ function extraxtODSCellText(cell) {
|
||||
/**
|
||||
* Extracts raw table content from an ODS file.
|
||||
* @param {ArrayBuffer} arrayBuffer - The ODS file.
|
||||
* @param {(str: string) => Document} parseXML - Function to parse XML content.
|
||||
* @returns {Promise<Map<SheetName, SheetRawContent>>}
|
||||
*/
|
||||
export async function getODSTableRawContent(arrayBuffer) {
|
||||
const zipDataReader = new Uint8ArrayReader(new Uint8Array(arrayBuffer));
|
||||
const zipReader = new ZipReader(zipDataReader);
|
||||
const zipEntries = await zipReader.getEntries()
|
||||
await zipReader.close();
|
||||
|
||||
/** @type {Map<Entry['filename'], Entry>} */
|
||||
const entryByFilename = new Map()
|
||||
for(const entry of zipEntries){
|
||||
const filename = entry.filename
|
||||
entryByFilename.set(filename, entry)
|
||||
}
|
||||
|
||||
const contentXmlEntry = entryByFilename.get('content.xml')
|
||||
|
||||
if(!contentXmlEntry){
|
||||
throw new TypeError(`entry 'content.xml' manquante dans le zip`)
|
||||
}
|
||||
export async function _getODSTableRawContent(arrayBuffer, parseXML) {
|
||||
const zip = await unzip(arrayBuffer);
|
||||
const entries = zip.entries;
|
||||
|
||||
// Extract the content.xml file which contains the spreadsheet data
|
||||
|
||||
//@ts-ignore
|
||||
const contentXml = await contentXmlEntry.getData(new TextWriter());
|
||||
//console.log('contentXml', contentXml);
|
||||
|
||||
const contentXml = await entries['content.xml'].text();
|
||||
const contentDoc = parseXML(contentXml);
|
||||
|
||||
const tableMap = new Map();
|
||||
@ -125,9 +102,72 @@ export async function getODSTableRawContent(arrayBuffer) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts raw table content from an XLSX file.
|
||||
* @param {ArrayBuffer} arrayBuffer - The XLSX file.
|
||||
* @param {(str: string) => Document} parseXML - Function to parse XML content.
|
||||
* @returns {Promise<Map<SheetName, SheetRawContent>>}
|
||||
*/
|
||||
export async function _getXLSXTableRawContent(arrayBuffer, parseXML) {
|
||||
const zip = await unzip(arrayBuffer);
|
||||
const entries = zip.entries;
|
||||
|
||||
const sharedStringsXml = await entries['xl/sharedStrings.xml'].text();
|
||||
const sharedStringsDoc = parseXML(sharedStringsXml);
|
||||
const sharedStrings = Array.from(sharedStringsDoc.getElementsByTagName('sst')[0].getElementsByTagName('si')).map(si => si.textContent);
|
||||
|
||||
// Get sheet names and their corresponding XML files
|
||||
const workbookXml = await entries['xl/workbook.xml'].text();
|
||||
const workbookDoc = parseXML(workbookXml);
|
||||
const sheets = Array.from(workbookDoc.getElementsByTagName('sheets')[0].getElementsByTagName('sheet'));
|
||||
const sheetNames = sheets.map(sheet => sheet.getAttribute('name'));
|
||||
const sheetIds = sheets.map(sheet => sheet.getAttribute('r:id'));
|
||||
|
||||
// Read the relations to get the actual filenames for each sheet
|
||||
const workbookRelsXml = await entries['xl/_rels/workbook.xml.rels'].text();
|
||||
const workbookRelsDoc = parseXML(workbookRelsXml);
|
||||
const sheetRels = Array.from(workbookRelsDoc.getElementsByTagName('Relationship'));
|
||||
const sheetFiles = sheetIds.map(id => sheetRels.find(rel => rel.getAttribute('Id') === id).getAttribute('Target').replace('worksheets/', ''));
|
||||
|
||||
// Read each sheet's XML and extract data in parallel
|
||||
const sheetDataPs = sheetFiles.map((sheetFile, index) => (
|
||||
entries[`xl/worksheets/${sheetFile}`].text().then(sheetXml => {
|
||||
const sheetDoc = parseXML(sheetXml);
|
||||
|
||||
const rows = sheetDoc.getElementsByTagName('sheetData')[0].getElementsByTagName('row');
|
||||
const sheetData = [];
|
||||
|
||||
for (let row of Array.from(rows)) {
|
||||
const cells = row.getElementsByTagName('c');
|
||||
const rowData = [];
|
||||
|
||||
for (let cell of Array.from(cells)) {
|
||||
const cellType = cell.getAttribute('t') || 'n';
|
||||
let cellValue = cell.getElementsByTagName('v')[0]?.textContent || '';
|
||||
|
||||
if (cellType === 's') {
|
||||
cellValue = sharedStrings[parseInt(cellValue, 10)];
|
||||
}
|
||||
|
||||
rowData.push({
|
||||
value: cellValue,
|
||||
type: cellType
|
||||
});
|
||||
}
|
||||
|
||||
sheetData.push(rowData);
|
||||
}
|
||||
|
||||
return [sheetNames[index], sheetData];
|
||||
})
|
||||
));
|
||||
|
||||
return new Map(await Promise.all(sheetDataPs));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function convertCellValue({value, type}) {
|
||||
@ -161,37 +201,21 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentSheets
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentSheets
|
||||
* @returns {Map<SheetName, ReturnType<convertCellValue>[][]>}
|
||||
*/
|
||||
export function tableRawContentToValues(rawContentSheets){
|
||||
return new Map(
|
||||
[...rawContentSheets].map(([sheetName, rawContent]) => {
|
||||
return [
|
||||
sheetName,
|
||||
sheetName,
|
||||
rawContent
|
||||
.map(row => row.map(c => convertCellValue(c)))
|
||||
]
|
||||
@ -204,7 +228,7 @@ export function tableRawContentToValues(rawContentSheets){
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {SheetCellRawContent} rawContentCell
|
||||
* @returns {string}
|
||||
*/
|
||||
@ -213,8 +237,8 @@ export function cellRawContentToStrings(rawContentCell){
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SheetRowRawContent} rawContentRow
|
||||
*
|
||||
* @param {SheetRowRawContent} rawContentRow
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function rowRawContentToStrings(rawContentRow){
|
||||
@ -222,8 +246,8 @@ export function rowRawContentToStrings(rawContentRow){
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SheetRawContent} rawContentSheet
|
||||
*
|
||||
* @param {SheetRawContent} rawContentSheet
|
||||
* @returns {string[][]}
|
||||
*/
|
||||
export function sheetRawContentToStrings(rawContentSheet){
|
||||
@ -231,8 +255,8 @@ export function sheetRawContentToStrings(rawContentSheet){
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentSheets
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentSheets
|
||||
* @returns {Map<SheetName, string[][]>}
|
||||
*/
|
||||
export function tableRawContentToStrings(rawContentSheets){
|
||||
@ -253,16 +277,16 @@ export function tableRawContentToStrings(rawContentSheets){
|
||||
|
||||
/**
|
||||
* 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[]}
|
||||
*/
|
||||
export function sheetRawContentToObjects(rawContent){
|
||||
let [firstRow, ...dataRows] = rawContent
|
||||
|
||||
/** @type {string[]} */
|
||||
|
||||
|
||||
const columns = firstRow.map((r, i) => {
|
||||
if (r.value === undefined || r.value === null || r.value === "") {
|
||||
return `Column ${i+1}`
|
||||
@ -284,8 +308,8 @@ export function sheetRawContentToObjects(rawContent){
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentSheets
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentSheets
|
||||
* @returns {Map<SheetName, any[]>}
|
||||
*/
|
||||
export function tableRawContentToObjects(rawContentSheets){
|
||||
@ -312,7 +336,7 @@ export function isCellFilled({value}){
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SheetRowRawContent} rawContentRow
|
||||
* @param {SheetRowRawContent} rawContentRow
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRowNotEmpty(rawContentRow){
|
||||
@ -320,7 +344,7 @@ export function isRowNotEmpty(rawContentRow){
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SheetRawContent} sheet
|
||||
* @param {SheetRawContent} sheet
|
||||
* @returns {SheetRawContent}
|
||||
*/
|
||||
export function removeEmptyRowsFromSheet(sheet){
|
||||
@ -329,8 +353,8 @@ export function removeEmptyRowsFromSheet(sheet){
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentTable
|
||||
*
|
||||
* @param {Map<SheetName, SheetRawContent>} rawContentTable
|
||||
* @returns {Map<SheetName, SheetRawContent>}
|
||||
*/
|
||||
export function tableWithoutEmptyRows(rawContentTable){
|
||||
@ -339,4 +363,4 @@ export function tableWithoutEmptyRows(rawContentTable){
|
||||
return [sheetName, removeEmptyRowsFromSheet(rawContent)]
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -10,12 +10,4 @@
|
||||
|
||||
/** @typedef {string} SheetName */
|
||||
|
||||
/**
|
||||
* @typedef OdfjsImage
|
||||
* @prop {ArrayBuffer} content
|
||||
* @prop {string} fileName
|
||||
* @prop {string} mediaType
|
||||
*
|
||||
*/
|
||||
|
||||
export {}
|
||||
@ -2,9 +2,9 @@ import {readFile} from 'node:fs/promises'
|
||||
|
||||
import test from 'ava';
|
||||
|
||||
import {getODSTableRawContent} from '../exports.js'
|
||||
import {getODSTableRawContent} from '../scripts/node.js'
|
||||
|
||||
const nomAgeContent = (await readFile('./tests/fixtures/nom-age.ods')).buffer
|
||||
const nomAgeContent = (await readFile('./tests/data/nom-age.ods')).buffer
|
||||
|
||||
test('basic', async t => {
|
||||
const table = await getODSTableRawContent(nomAgeContent);
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import test from 'ava';
|
||||
|
||||
import {getODSTableRawContent, createOdsFile} from '../exports.js'
|
||||
|
||||
/** @import {SheetName, SheetRawContent} from '../scripts/types.js' */
|
||||
import {getODSTableRawContent, createOdsFile} from '../scripts/node.js'
|
||||
|
||||
test('basic file creation', async t => {
|
||||
/** @type {Map<SheetName, SheetRawContent>} */
|
||||
const content = new Map([
|
||||
[
|
||||
'La feuille',
|
||||
@ -18,7 +15,7 @@ test('basic file creation', async t => {
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const odsFile = await createOdsFile(content)
|
||||
|
||||
const parsedContent = await getODSTableRawContent(odsFile)
|
||||
|
||||
@ -1,36 +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('basic template filling with variable substitution', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/template-anniversaire.odt')
|
||||
const templateContent = `Yo {nom} !
|
||||
Tu es né.e le {dateNaissance}
|
||||
|
||||
Bonjoir ☀️
|
||||
`
|
||||
|
||||
const data = {
|
||||
nom: 'David Bruant',
|
||||
dateNaissance: '8 mars 1987'
|
||||
}
|
||||
|
||||
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, `Yo David Bruant !
|
||||
Tu es né.e le 8 mars 1987
|
||||
|
||||
Bonjoir ☀️
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
@ -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,`)
|
||||
|
||||
});
|
||||
|
||||
@ -1,375 +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('basic template filling with {#each}', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/enum-courses.odt')
|
||||
const templateContent = `🧺 La liste de courses incroyable 🧺
|
||||
|
||||
{#each listeCourses as élément}
|
||||
{élément}
|
||||
{/each}
|
||||
`
|
||||
|
||||
const data = {
|
||||
listeCourses : [
|
||||
'Radis',
|
||||
`Jus d'orange`,
|
||||
'Pâtes à lasagne (fraîches !)'
|
||||
]
|
||||
}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
|
||||
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||
try{
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent, `🧺 La liste de courses incroyable 🧺
|
||||
|
||||
Radis
|
||||
Jus d'orange
|
||||
Pâtes à lasagne (fraîches !)
|
||||
`)
|
||||
}catch(e){console.error(e); throw e}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('Filling with {#each} and non-iterable value results in no error and empty result', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/enum-courses.odt')
|
||||
const templateContent = `🧺 La liste de courses incroyable 🧺
|
||||
|
||||
{#each listeCourses as élément}
|
||||
{élément}
|
||||
{/each}
|
||||
`
|
||||
|
||||
const data = {
|
||||
listeCourses : undefined
|
||||
}
|
||||
|
||||
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, `🧺 La liste de courses incroyable 🧺
|
||||
|
||||
`)
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling with {#each} generating a list', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/liste-courses.odt')
|
||||
const templateContent = `🧺 La liste de courses incroyable 🧺
|
||||
|
||||
- {#each listeCourses as élément}
|
||||
- {élément}
|
||||
- {/each}
|
||||
`
|
||||
|
||||
const data = {
|
||||
listeCourses : [
|
||||
'Radis',
|
||||
`Jus d'orange`,
|
||||
'Pâtes à lasagne (fraîches !)'
|
||||
]
|
||||
}
|
||||
|
||||
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, `🧺 La liste de courses incroyable 🧺
|
||||
|
||||
- Radis
|
||||
- Jus d'orange
|
||||
- Pâtes à lasagne (fraîches !)
|
||||
`)
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling with 2 sequential {#each}', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/liste-fruits-et-légumes.odt')
|
||||
const templateContent = `Liste de fruits et légumes
|
||||
|
||||
Fruits
|
||||
{#each fruits as fruit}
|
||||
{fruit}
|
||||
{/each}
|
||||
|
||||
Légumes
|
||||
{#each légumes as légume}
|
||||
{légume}
|
||||
{/each}
|
||||
`
|
||||
|
||||
const data = {
|
||||
fruits : [
|
||||
'Pastèque 🍉',
|
||||
`Kiwi 🥝`,
|
||||
'Banane 🍌'
|
||||
],
|
||||
légumes: [
|
||||
'Champignon 🍄🟫',
|
||||
'Avocat 🥑',
|
||||
'Poivron 🫑'
|
||||
]
|
||||
}
|
||||
|
||||
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, `Liste de fruits et légumes
|
||||
|
||||
Fruits
|
||||
Pastèque 🍉
|
||||
Kiwi 🥝
|
||||
Banane 🍌
|
||||
|
||||
Légumes
|
||||
Champignon 🍄🟫
|
||||
Avocat 🥑
|
||||
Poivron 🫑
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling with nested {#each}s', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/légumes-de-saison.odt')
|
||||
const templateContent = `Légumes de saison
|
||||
|
||||
{#each légumesSaison as saisonLégumes}
|
||||
{saisonLégumes.saison}
|
||||
- {#each saisonLégumes.légumes as légume}
|
||||
- {légume}
|
||||
- {/each}
|
||||
|
||||
{/each}
|
||||
`
|
||||
|
||||
const data = {
|
||||
légumesSaison : [
|
||||
{
|
||||
saison: 'Printemps',
|
||||
légumes: [
|
||||
'Asperge',
|
||||
'Betterave',
|
||||
'Blette'
|
||||
]
|
||||
},
|
||||
{
|
||||
saison: 'Été',
|
||||
légumes: [
|
||||
'Courgette',
|
||||
'Poivron',
|
||||
'Laitue'
|
||||
]
|
||||
},
|
||||
{
|
||||
saison: 'Automne',
|
||||
légumes: [
|
||||
'Poireau',
|
||||
'Potiron',
|
||||
'Brocoli'
|
||||
]
|
||||
},
|
||||
{
|
||||
saison: 'Hiver',
|
||||
légumes: [
|
||||
'Radis',
|
||||
'Chou de Bruxelles',
|
||||
'Frisée'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent, `Légumes de saison
|
||||
|
||||
Printemps
|
||||
- Asperge
|
||||
- Betterave
|
||||
- Blette
|
||||
|
||||
Été
|
||||
- Courgette
|
||||
- Poivron
|
||||
- Laitue
|
||||
|
||||
Automne
|
||||
- Poireau
|
||||
- Potiron
|
||||
- Brocoli
|
||||
|
||||
Hiver
|
||||
- Radis
|
||||
- Chou de Bruxelles
|
||||
- Frisée
|
||||
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling with text after {/each} in same text node', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/text-after-closing-each.odt')
|
||||
const templateContent = `Légumes de saison
|
||||
|
||||
{#each légumes as légume}
|
||||
{légume},
|
||||
{/each} en {saison}
|
||||
`
|
||||
|
||||
const data = {
|
||||
saison: 'Printemps',
|
||||
légumes: [
|
||||
'Asperge',
|
||||
'Betterave',
|
||||
'Blette'
|
||||
]
|
||||
}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent, `Légumes de saison
|
||||
|
||||
Asperge,
|
||||
Betterave,
|
||||
Blette,
|
||||
en Printemps
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling of a table', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/tableau-simple.odt')
|
||||
const templateContent = `Évolution énergie en kWh par personne en France
|
||||
|
||||
Année
|
||||
Énergie par personne
|
||||
{#each annéeConsos as annéeConso}
|
||||
|
||||
{annéeConso.année}
|
||||
{annéeConso.conso}
|
||||
{/each}
|
||||
`
|
||||
|
||||
/*
|
||||
Data sources:
|
||||
|
||||
U.S. Energy Information Administration (2023)Energy Institute -
|
||||
Statistical Review of World Energy (2024)Population based on various sources (2023)
|
||||
|
||||
– with major processing by Our World in Data
|
||||
*/
|
||||
const data = {
|
||||
annéeConsos : [
|
||||
{ année: 1970, conso: 36252.637},
|
||||
{ année: 1980, conso: 43328.78},
|
||||
{ année: 1990, conso: 46971.94},
|
||||
{ année: 2000, conso: 53147.277},
|
||||
{ année: 2010, conso: 48062.32},
|
||||
{ année: 2020, conso: 37859.246},
|
||||
]
|
||||
}
|
||||
|
||||
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(), `Évolution énergie en kWh par personne en France
|
||||
|
||||
Année
|
||||
Énergie par personne
|
||||
1970
|
||||
36252.637
|
||||
1980
|
||||
43328.78
|
||||
1990
|
||||
46971.94
|
||||
2000
|
||||
53147.277
|
||||
2010
|
||||
48062.32
|
||||
2020
|
||||
37859.246
|
||||
`.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,211 +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 filling with several layers of formatting in {#each ...} start marker', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-plusieurs-couches.odt')
|
||||
const templateContent = `Liste de nombres
|
||||
|
||||
Les nombres : {#each nombres as n}{n} {/each} !!
|
||||
`
|
||||
|
||||
const data = {
|
||||
nombres : [1,2,3,5]
|
||||
}
|
||||
|
||||
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, `Liste de nombres
|
||||
|
||||
Les nombres : 1 2 3 5 !!
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - both {#each ...} and {/each} within the same Text node are formatted', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-2-markeurs-formatted.odt')
|
||||
const templateContent = `Liste de nombres
|
||||
|
||||
Les nombres : {#each nombres as n}{n} {/each} !!
|
||||
`
|
||||
|
||||
const data = {
|
||||
nombres : [2,3,5,8]
|
||||
}
|
||||
|
||||
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, `Liste de nombres
|
||||
|
||||
Les nombres : 2 3 5 8 !!
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - {#each ...} and text before partially formatted', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-each-start-and-before-formatted.odt')
|
||||
const templateContent = `Liste de nombres
|
||||
|
||||
Les nombres : {#each nombres as n}{n} {/each} !!
|
||||
`
|
||||
|
||||
const data = {
|
||||
nombres : [3,5,8, 13]
|
||||
}
|
||||
|
||||
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, `Liste de nombres
|
||||
|
||||
Les nombres : 3 5 8 13 !!
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - {/each} and text after partially formatted', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-each-end-and-after-formatted.odt')
|
||||
const templateContent = `Liste de nombres
|
||||
|
||||
Les nombres : {#each nombres as n}{n} {/each} !!
|
||||
`
|
||||
|
||||
const data = {
|
||||
nombres : [5,8, 13, 21]
|
||||
}
|
||||
|
||||
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, `Liste de nombres
|
||||
|
||||
Les nombres : 5 8 13 21 !!
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - partially formatted variable', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/partially-formatted-variable.odt')
|
||||
const templateContent = `Nombre
|
||||
|
||||
Voici le nombre : {nombre} !!!
|
||||
`
|
||||
|
||||
const data = {nombre : 37}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||
//try{
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
//}catch(e){console.error(e)}
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent, `Nombre
|
||||
|
||||
Voici le nombre : 37 !!!
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - formatted-start-each-single-paragraph', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/formatted-start-each-single-paragraph.odt')
|
||||
const templateContent = `
|
||||
{#each nombres as n}
|
||||
{n}
|
||||
{/each}
|
||||
`
|
||||
|
||||
const data = {nombres : [37, 38, 39]}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template')
|
||||
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent, `
|
||||
|
||||
37
|
||||
38
|
||||
39
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - formatted ghost if then', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/ghost-if.odt')
|
||||
const templateContent = `
|
||||
Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if}
|
||||
`
|
||||
|
||||
const data = {scientifique: {source_lumineuses: true}}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template')
|
||||
let odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent.trim(), `
|
||||
Utilisation de sources lumineuses : Oui
|
||||
`.trim())
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling - formatted ghost if else', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/ghost-if.odt')
|
||||
const templateContent = `
|
||||
Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if}
|
||||
`
|
||||
|
||||
const data = {scientifique: {source_lumineuses: false}}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template')
|
||||
let odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent.trim(), `
|
||||
Utilisation de sources lumineuses : Non
|
||||
`.trim())
|
||||
|
||||
});
|
||||
@ -1,91 +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('basic template filling with {#if}{:else} - then 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')
|
||||
|
||||
// then branch
|
||||
const odtResult3 = await fillOdtTemplate(odtTemplate, {n: 3})
|
||||
const odtResult3TextContent = await getOdtTextContent(odtResult3)
|
||||
t.deepEqual(odtResult3TextContent, `Description du nombre 3
|
||||
|
||||
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
|
||||
const odtResult8 = await fillOdtTemplate(odtTemplate, {n: 8})
|
||||
const odtResult8TextContent = await getOdtTextContent(odtResult8)
|
||||
t.deepEqual(odtResult8TextContent, `Description du nombre 8
|
||||
|
||||
n est un grand nombre
|
||||
`)
|
||||
}
|
||||
catch(e){console.error(e); throw e}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('complex structured if', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/left-branch-content-and-two-consecutive-ifs.odt')
|
||||
const templateContent = `Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if}
|
||||
{#if scientifique.source_lumineuses && scientifique.modalités_source_lumineuses }
|
||||
Modalités d’utilisation de sources lumineuses : {scientifique.modalités_source_lumineuses}
|
||||
{/if}
|
||||
`
|
||||
|
||||
const data = {
|
||||
scientifique: {
|
||||
source_lumineuses: false,
|
||||
//modalités_source_lumineuses: 'lampes torches'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template')
|
||||
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
const odtResultTextContent = await getOdtTextContent(odtResult)
|
||||
t.deepEqual(odtResultTextContent.trim(), `Utilisation de sources lumineuses : Non`)
|
||||
|
||||
});
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
import test from 'ava';
|
||||
import {join} from 'node:path';
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js'
|
||||
|
||||
import {fillOdtTemplate, getOdtTextContent} from '../../exports.js'
|
||||
import { listZipEntries } from '../helpers/zip-analysis.js';
|
||||
import { getContentDocument } from '../../scripts/odf/odt/getOdtTextContent.js';
|
||||
|
||||
|
||||
test('template filling preserves images', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/template-avec-image.odt')
|
||||
|
||||
const data = {
|
||||
commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶`
|
||||
}
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
const templateEntries = await listZipEntries(odtTemplate)
|
||||
|
||||
//console.log('templateEntries', templateEntries.map(({filename, directory}) => ({filename, directory})))
|
||||
|
||||
t.assert(
|
||||
templateEntries.find(entry => entry.filename.startsWith('Pictures/')),
|
||||
`One zip entry of the template is expected to have a name that starts with 'Pictures/'`
|
||||
)
|
||||
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
const resultEntries = await listZipEntries(odtResult)
|
||||
|
||||
//console.log('resultEntries', resultEntries.map(({filename, directory}) => ({filename, directory})))
|
||||
|
||||
|
||||
t.assert(
|
||||
resultEntries.find(entry => entry.filename.startsWith('Pictures/')),
|
||||
`One zip entry of the result is expected to have a name that starts with 'Pictures/'`
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
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.')
|
||||
|
||||
})
|
||||
@ -1,65 +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 filling {#if ...}{/if} within a single text node', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/inline-if-nombres.odt')
|
||||
const templateContent = `Taille de nombre
|
||||
|
||||
Le nombre {n} est {#if n<5}petit{:else}grand{/if}.
|
||||
`
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
|
||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||
|
||||
const odtResult3 = await fillOdtTemplate(odtTemplate, {n : 3})
|
||||
|
||||
const odtResult3TextContent = await getOdtTextContent(odtResult3)
|
||||
t.deepEqual(odtResult3TextContent, `Taille de nombre
|
||||
|
||||
Le nombre 3 est petit.
|
||||
`)
|
||||
|
||||
const odtResult9 = await fillOdtTemplate(odtTemplate, {n : 9})
|
||||
|
||||
const odtResult9TextContent = await getOdtTextContent(odtResult9)
|
||||
t.deepEqual(odtResult9TextContent, `Taille de nombre
|
||||
|
||||
Le nombre 9 est grand.
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('template filling {#each ...}{/each} within a single text node', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/liste-nombres.odt')
|
||||
const templateContent = `Liste de nombres
|
||||
|
||||
Les nombres : {#each nombres as n}{n} {/each} !!
|
||||
`
|
||||
|
||||
const data = {
|
||||
nombres : [1,1,2,3,5,8,13,21]
|
||||
}
|
||||
|
||||
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, `Liste de nombres
|
||||
|
||||
Les nombres : 1 1 2 3 5 8 13 21 !!
|
||||
`)
|
||||
|
||||
});
|
||||
|
||||
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/description-nombre.odt
vendored
BIN
tests/fixtures/description-nombre.odt
vendored
Binary file not shown.
BIN
tests/fixtures/enum-courses.odt
vendored
BIN
tests/fixtures/enum-courses.odt
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/ghost-if.odt
vendored
BIN
tests/fixtures/ghost-if.odt
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.
BIN
tests/fixtures/inline-if-nombres.odt
vendored
BIN
tests/fixtures/inline-if-nombres.odt
vendored
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/liste-courses.odt
vendored
BIN
tests/fixtures/liste-courses.odt
vendored
Binary file not shown.
BIN
tests/fixtures/liste-fruits-et-légumes.odt
vendored
BIN
tests/fixtures/liste-fruits-et-légumes.odt
vendored
Binary file not shown.
BIN
tests/fixtures/liste-nombres.odt
vendored
BIN
tests/fixtures/liste-nombres.odt
vendored
Binary file not shown.
BIN
tests/fixtures/légumes-de-saison.odt
vendored
BIN
tests/fixtures/légumes-de-saison.odt
vendored
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/partially-formatted-variable.odt
vendored
BIN
tests/fixtures/partially-formatted-variable.odt
vendored
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 |
BIN
tests/fixtures/tableau-simple.odt
vendored
BIN
tests/fixtures/tableau-simple.odt
vendored
Binary file not shown.
BIN
tests/fixtures/template-anniversaire.odt
vendored
BIN
tests/fixtures/template-anniversaire.odt
vendored
Binary file not shown.
BIN
tests/fixtures/template-avec-image.odt
vendored
BIN
tests/fixtures/template-avec-image.odt
vendored
Binary file not shown.
BIN
tests/fixtures/text-after-closing-each.odt
vendored
BIN
tests/fixtures/text-after-closing-each.odt
vendored
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
import { ZipReader, Uint8ArrayReader } from '@zip.js/zip.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ArrayBuffer} odtTemplate
|
||||
* @returns {ReturnType<typeof ZipReader.prototype.getEntries>}
|
||||
*/
|
||||
export async function listZipEntries(odtTemplate){
|
||||
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtTemplate)));
|
||||
return reader.getEntries();
|
||||
}
|
||||
@ -2,10 +2,10 @@ import {readFile} from 'node:fs/promises'
|
||||
|
||||
import test from 'ava';
|
||||
|
||||
import {getODSTableRawContent} from '../exports.js'
|
||||
import {getODSTableRawContent} from '../scripts/node.js'
|
||||
|
||||
test('.ods file with table:number-columns-repeated attribute in cell', async t => {
|
||||
const repeatedCellFileContent = (await readFile('./tests/fixtures/cellules-répétées.ods')).buffer
|
||||
const repeatedCellFileContent = (await readFile('./tests/data/cellules-répétées.ods')).buffer
|
||||
|
||||
const table = await getODSTableRawContent(repeatedCellFileContent);
|
||||
|
||||
@ -17,7 +17,7 @@ test('.ods file with table:number-columns-repeated attribute in cell', async t =
|
||||
|
||||
|
||||
test('.ods cells with dates should be recognized', async t => {
|
||||
const odsFileWithDates = (await readFile('./tests/fixtures/cellules avec dates.ods')).buffer
|
||||
const odsFileWithDates = (await readFile('./tests/data/cellules avec dates.ods')).buffer
|
||||
const table = await getODSTableRawContent(odsFileWithDates);
|
||||
|
||||
const feuille1 = table.get('Feuille1')
|
||||
@ -39,7 +39,7 @@ test('.ods cells with dates should be recognized', async t => {
|
||||
|
||||
|
||||
test('.ods file with new lines in content is ', async t => {
|
||||
const repeatedCellFileContent = (await readFile('./tests/fixtures/cellule avec sauts.ods')).buffer
|
||||
const repeatedCellFileContent = (await readFile('./tests/data/cellule avec sauts.ods')).buffer
|
||||
|
||||
const table = await getODSTableRawContent(repeatedCellFileContent);
|
||||
|
||||
@ -55,34 +55,4 @@ Pourquoi t'aimeraient-ils seulement comme tu es ? (hein)
|
||||
Si t'es pas comme eux quand t'es naturel`
|
||||
|
||||
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,5 +1,5 @@
|
||||
import test from 'ava';
|
||||
import { sheetRawContentToObjects } from "../exports.js"
|
||||
import { sheetRawContentToObjects } from "../scripts/shared.js"
|
||||
|
||||
test("Empty header value should be kept", t => {
|
||||
const rawContent = [
|
||||
|
||||
@ -1,149 +0,0 @@
|
||||
import {writeFile, readFile} from 'node:fs/promises'
|
||||
import {join} from 'node:path';
|
||||
|
||||
import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js'
|
||||
import {fillOdtTemplate} from '../exports.js'
|
||||
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/template-anniversaire.odt')
|
||||
const data = {
|
||||
nom: 'David Bruant',
|
||||
dateNaissance: '8 mars 1987'
|
||||
}
|
||||
*/
|
||||
|
||||
/*const templatePath = join(import.meta.dirname, '../tests/fixtures/enum-courses.odt')
|
||||
const data = {
|
||||
listeCourses : [
|
||||
'Radis',
|
||||
`Jus d'orange`,
|
||||
'Pâtes à lasagne (fraîches !)'
|
||||
]
|
||||
}*/
|
||||
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/liste-fruits-et-légumes.odt')
|
||||
const data = {
|
||||
fruits : [
|
||||
'Pastèque 🍉',
|
||||
`Kiwi 🥝`,
|
||||
'Banane 🍌'
|
||||
],
|
||||
légumes: [
|
||||
'Champignon 🍄🟫',
|
||||
'Avocat 🥑',
|
||||
'Poivron 🫑'
|
||||
]
|
||||
}*/
|
||||
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/légumes-de-saison.odt')
|
||||
const data = {
|
||||
légumesSaison : [
|
||||
{
|
||||
saison: 'Printemps',
|
||||
légumes: [
|
||||
'Asperge',
|
||||
'Betterave',
|
||||
'Blette'
|
||||
]
|
||||
},
|
||||
{
|
||||
saison: 'Été',
|
||||
légumes: [
|
||||
'Courgette',
|
||||
'Poivron',
|
||||
'Laitue'
|
||||
]
|
||||
},
|
||||
{
|
||||
saison: 'Automne',
|
||||
légumes: [
|
||||
'Poireau',
|
||||
'Potiron',
|
||||
'Brocoli'
|
||||
]
|
||||
},
|
||||
{
|
||||
saison: 'Hiver',
|
||||
légumes: [
|
||||
'Radis',
|
||||
'Chou de Bruxelles',
|
||||
'Frisée'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/tableau-simple.odt')
|
||||
const data = {
|
||||
annéeConsos : [
|
||||
{ année: 1970, conso: 36252.637},
|
||||
{ année: 1980, conso: 43328.78},
|
||||
{ année: 1990, conso: 46971.94},
|
||||
{ année: 2000, conso: 53147.277},
|
||||
{ année: 2010, conso: 48062.32},
|
||||
{ année: 2020, conso: 37859.246},
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/template-avec-image.odt')
|
||||
|
||||
const data = {
|
||||
commentaire : `J'adooooooore 🤩 West covinaaaaaaaaaaa 🎶`
|
||||
}
|
||||
*/
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/partially-formatted-variable.odt')
|
||||
const data = {nombre : 37}
|
||||
*/
|
||||
|
||||
/*
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
|
||||
const data = {
|
||||
saison: 'Printemps',
|
||||
légumes: [
|
||||
'Asperge',
|
||||
'Betterave',
|
||||
'Blette'
|
||||
]
|
||||
}
|
||||
*/
|
||||
// const templatePath = join(import.meta.dirname, '../tests/fixtures/text-after-closing-each.odt')
|
||||
// const data = {
|
||||
// saison: 'Printemps',
|
||||
// légumes: [
|
||||
// 'Asperge',
|
||||
// 'Betterave',
|
||||
// 'Blette'
|
||||
// ]
|
||||
// }
|
||||
|
||||
// const templatePath = join(import.meta.dirname, '../tests/fixtures/if-then-each.odt')
|
||||
// const data = {liste_départements : ['95', '33']}
|
||||
|
||||
|
||||
const templatePath = join(import.meta.dirname, '../tests/fixtures/basic-image-insertion.odt')
|
||||
const photo1Path = join(import.meta.dirname, '../tests/fixtures/pitchou-1.png')
|
||||
const photo2Path = join(import.meta.dirname, '../tests/fixtures/pitchou-2.png')
|
||||
|
||||
const photo1Buffer = (await readFile(photo1Path)).buffer
|
||||
const photo2Buffer = (await readFile(photo2Path)).buffer
|
||||
|
||||
const photos = [{content: photo1Buffer, fileName: 'pitchou-1.png', mediaType: 'image/png'}, {content: photo2Buffer, fileName: 'pitchou-2.png', mediaType: 'image/png'}]
|
||||
|
||||
const data = {
|
||||
title: 'Titre de mon projet',
|
||||
photos,
|
||||
}
|
||||
|
||||
|
||||
const odtTemplate = await getOdtTemplate(templatePath)
|
||||
const odtResult = await fillOdtTemplate(odtTemplate, data)
|
||||
|
||||
writeFile('yo.odt', new Uint8Array(odtResult))
|
||||
Loading…
x
Reference in New Issue
Block a user