Expression evaluation based on ses Compartments
This commit is contained in:
parent
9cb48a811a
commit
66cd2cffda
35
package-lock.json
generated
35
package-lock.json
generated
@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "ods-xlsx",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ods-xlsx",
|
"name": "@odfjs/odfjs",
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
"@zip.js/zip.js": "^2.7.57"
|
"@zip.js/zip.js": "^2.7.57",
|
||||||
|
"ses": "^1.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
@ -40,6 +41,12 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@endo/env-options": {
|
||||||
|
"version": "1.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz",
|
||||||
|
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||||
@ -3607,6 +3614,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ses": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@endo/env-options": "^1.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/set-blocking": {
|
"node_modules/set-blocking": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
@ -4555,6 +4571,11 @@
|
|||||||
"@jridgewell/trace-mapping": "^0.3.9"
|
"@jridgewell/trace-mapping": "^0.3.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@endo/env-options": {
|
||||||
|
"version": "1.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz",
|
||||||
|
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg=="
|
||||||
|
},
|
||||||
"@jridgewell/gen-mapping": {
|
"@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||||
@ -7116,6 +7137,14 @@
|
|||||||
"type-fest": "^0.13.1"
|
"type-fest": "^0.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ses": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==",
|
||||||
|
"requires": {
|
||||||
|
"@endo/env-options": "^1.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"set-blocking": {
|
"set-blocking": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
"@zip.js/zip.js": "^2.7.57"
|
"@zip.js/zip.js": "^2.7.57",
|
||||||
|
"ses": "^1.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayRea
|
|||||||
import {traverse, parseXML, serializeToString, Node} from '../DOMUtils.js'
|
import {traverse, parseXML, serializeToString, Node} from '../DOMUtils.js'
|
||||||
import {makeManifestFile, getManifestFileData} from './manifest.js';
|
import {makeManifestFile, getManifestFileData} from './manifest.js';
|
||||||
|
|
||||||
|
import 'ses'
|
||||||
|
|
||||||
|
lockdown();
|
||||||
|
|
||||||
/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */
|
/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */
|
||||||
/** @import {ODFManifest} from './manifest.js' */
|
/** @import {ODFManifest} from './manifest.js' */
|
||||||
|
|
||||||
@ -18,41 +22,17 @@ const ODTMimetype = 'application/vnd.oasis.opendocument.text'
|
|||||||
/**
|
/**
|
||||||
* @typedef TextPlaceToFill
|
* @typedef TextPlaceToFill
|
||||||
* @property { {expression: string, replacedString:string}[] } expressions
|
* @property { {expression: string, replacedString:string}[] } expressions
|
||||||
* @property {(values: any) => void} fill
|
* @property {() => void} fill
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PPP : for now, expression is expected to be only an object property name or a dot-path
|
|
||||||
* in the future, it will certainly be a JavaScript expression
|
|
||||||
* securely evaluated within an hardernedJS Compartment https://hardenedjs.org/#compartment
|
|
||||||
* @param {string} expression
|
|
||||||
* @param {any} context - data / global object
|
|
||||||
* @return {any}
|
|
||||||
*/
|
|
||||||
function evaluateTemplateExpression(expression, context){
|
|
||||||
const parts = expression.trim().split('.')
|
|
||||||
|
|
||||||
let value = context;
|
|
||||||
|
|
||||||
for(const part of parts){
|
|
||||||
if(!value){
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
value = value[part]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
|
* @param {Compartment} compartment
|
||||||
* @returns {TextPlaceToFill | undefined}
|
* @returns {TextPlaceToFill | undefined}
|
||||||
*/
|
*/
|
||||||
function findPlacesToFillInString(str) {
|
function findPlacesToFillInString(str, compartment) {
|
||||||
const matches = str.matchAll(/\{([^{#\/]+?)\}/g)
|
const matches = str.matchAll(/\{([^{#\/]+?)\}/g)
|
||||||
|
|
||||||
/** @type {TextPlaceToFill['expressions']} */
|
/** @type {TextPlaceToFill['expressions']} */
|
||||||
@ -75,8 +55,7 @@ function findPlacesToFillInString(str) {
|
|||||||
if (fixedPart.length >= 1)
|
if (fixedPart.length >= 1)
|
||||||
parts.push(fixedPart)
|
parts.push(fixedPart)
|
||||||
|
|
||||||
|
parts.push(() => compartment.evaluate(expression))
|
||||||
parts.push(data => evaluateTemplateExpression(expression, data))
|
|
||||||
|
|
||||||
remaining = newRemaining
|
remaining = newRemaining
|
||||||
}
|
}
|
||||||
@ -117,10 +96,9 @@ function findPlacesToFillInString(str) {
|
|||||||
* @param {string} iterableExpression
|
* @param {string} iterableExpression
|
||||||
* @param {string} itemExpression
|
* @param {string} itemExpression
|
||||||
* @param {Node} endNode
|
* @param {Node} endNode
|
||||||
* @param {any} data
|
* @param {Compartment} compartment
|
||||||
* @param {typeof Node} Node
|
|
||||||
*/
|
*/
|
||||||
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, data, Node){
|
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment){
|
||||||
//console.log('fillEachBlock', iterableExpression, itemExpression)
|
//console.log('fillEachBlock', iterableExpression, itemExpression)
|
||||||
//console.log('startNode', startNode.nodeType, startNode.nodeName)
|
//console.log('startNode', startNode.nodeType, startNode.nodeName)
|
||||||
//console.log('endNode', endNode.nodeType, endNode.nodeName)
|
//console.log('endNode', endNode.nodeType, endNode.nodeName)
|
||||||
@ -189,7 +167,7 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, d
|
|||||||
|
|
||||||
// Find the iterable in the data
|
// Find the iterable in the data
|
||||||
// PPP eventually, evaluate the expression as a JS expression
|
// PPP eventually, evaluate the expression as a JS expression
|
||||||
let iterable = evaluateTemplateExpression(iterableExpression, data)
|
let iterable = compartment.evaluate(iterableExpression)
|
||||||
if(!iterable || typeof iterable[Symbol.iterator] !== 'function'){
|
if(!iterable || typeof iterable[Symbol.iterator] !== 'function'){
|
||||||
// when there is no iterable, silently replace with empty array
|
// when there is no iterable, silently replace with empty array
|
||||||
iterable = []
|
iterable = []
|
||||||
@ -202,11 +180,14 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, d
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const itemFragment = repeatedFragment.cloneNode(true)
|
const itemFragment = repeatedFragment.cloneNode(true)
|
||||||
|
|
||||||
|
let insideCompartment = new Compartment(
|
||||||
|
Object.assign({}, compartment.globalThis, {[itemExpression]: item})
|
||||||
|
)
|
||||||
|
|
||||||
// recursive call to fillTemplatedOdtElement on itemFragment
|
// recursive call to fillTemplatedOdtElement on itemFragment
|
||||||
fillTemplatedOdtElement(
|
fillTemplatedOdtElement(
|
||||||
itemFragment,
|
itemFragment,
|
||||||
Object.assign({}, data, {[itemExpression]: item}),
|
insideCompartment
|
||||||
Node
|
|
||||||
)
|
)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
commonAncestor.insertBefore(itemFragment, endChild)
|
commonAncestor.insertBefore(itemFragment, endChild)
|
||||||
@ -220,12 +201,11 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, d
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Element | DocumentFragment} rootElement
|
* @param {Element | DocumentFragment | Document} rootElement
|
||||||
* @param {any} data
|
* @param {Compartment} compartment
|
||||||
* @param {typeof Node} Node
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function fillTemplatedOdtElement(rootElement, data, Node){
|
function fillTemplatedOdtElement(rootElement, compartment){
|
||||||
//console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName)
|
//console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName)
|
||||||
|
|
||||||
// Perform a first traverse to split textnodes when they contain several block markers
|
// Perform a first traverse to split textnodes when they contain several block markers
|
||||||
@ -246,6 +226,7 @@ function fillTemplatedOdtElement(rootElement, data, Node){
|
|||||||
let thisMatch = remainingText.match(regexp)
|
let thisMatch = remainingText.match(regexp)
|
||||||
|
|
||||||
// trying to find only the first match in remainingText string
|
// trying to find only the first match in remainingText string
|
||||||
|
// @ts-ignore
|
||||||
if(thisMatch && (!match || match.index > thisMatch.index)){
|
if(thisMatch && (!match || match.index > thisMatch.index)){
|
||||||
match = thisMatch
|
match = thisMatch
|
||||||
}
|
}
|
||||||
@ -255,6 +236,7 @@ function fillTemplatedOdtElement(rootElement, data, Node){
|
|||||||
// split 3-way : before-match, match and after-match
|
// split 3-way : before-match, match and after-match
|
||||||
|
|
||||||
if(match[0].length < remainingText.length){
|
if(match[0].length < remainingText.length){
|
||||||
|
// @ts-ignore
|
||||||
let afterMatchTextNode = currentNode.splitText(match.index + match[0].length)
|
let afterMatchTextNode = currentNode.splitText(match.index + match[0].length)
|
||||||
if(afterMatchTextNode.textContent && afterMatchTextNode.textContent.length >= 1){
|
if(afterMatchTextNode.textContent && afterMatchTextNode.textContent.length >= 1){
|
||||||
remainingText = afterMatchTextNode.textContent
|
remainingText = afterMatchTextNode.textContent
|
||||||
@ -265,7 +247,9 @@ function fillTemplatedOdtElement(rootElement, data, Node){
|
|||||||
|
|
||||||
// per spec, currentNode now contains before-match and match text
|
// per spec, currentNode now contains before-match and match text
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
if(match.index > 0){
|
if(match.index > 0){
|
||||||
|
// @ts-ignore
|
||||||
currentNode.splitText(match.index)
|
currentNode.splitText(match.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,7 +328,7 @@ function fillTemplatedOdtElement(rootElement, data, Node){
|
|||||||
|
|
||||||
// found an #each and its corresponding /each
|
// found an #each and its corresponding /each
|
||||||
// execute replacement loop
|
// execute replacement loop
|
||||||
fillEachBlock(eachBlockStartNode, iterableExpression, itemExpression, eachBlockEndNode, data, Node)
|
fillEachBlock(eachBlockStartNode, iterableExpression, itemExpression, eachBlockEndNode, compartment)
|
||||||
|
|
||||||
eachBlockStartNode = undefined
|
eachBlockStartNode = undefined
|
||||||
iterableExpression = undefined
|
iterableExpression = undefined
|
||||||
@ -356,12 +340,16 @@ function fillTemplatedOdtElement(rootElement, data, Node){
|
|||||||
|
|
||||||
// Looking for variables for substitutions
|
// Looking for variables for substitutions
|
||||||
if(!insideAnEachBlock){
|
if(!insideAnEachBlock){
|
||||||
|
// @ts-ignore
|
||||||
if (currentNode.data) {
|
if (currentNode.data) {
|
||||||
const placesToFill = findPlacesToFillInString(currentNode.data)
|
// @ts-ignore
|
||||||
|
const placesToFill = findPlacesToFillInString(currentNode.data, compartment)
|
||||||
|
|
||||||
if(placesToFill){
|
if(placesToFill){
|
||||||
const newText = placesToFill.fill(data)
|
const newText = placesToFill.fill()
|
||||||
|
// @ts-ignore
|
||||||
const newTextNode = currentNode.ownerDocument?.createTextNode(newText)
|
const newTextNode = currentNode.ownerDocument?.createTextNode(newText)
|
||||||
|
// @ts-ignore
|
||||||
currentNode.parentNode?.replaceChild(newTextNode, currentNode)
|
currentNode.parentNode?.replaceChild(newTextNode, currentNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -374,10 +362,13 @@ function fillTemplatedOdtElement(rootElement, data, Node){
|
|||||||
if(currentNode.nodeType === Node.ATTRIBUTE_NODE){
|
if(currentNode.nodeType === Node.ATTRIBUTE_NODE){
|
||||||
// Looking for variables for substitutions
|
// Looking for variables for substitutions
|
||||||
if(!insideAnEachBlock){
|
if(!insideAnEachBlock){
|
||||||
|
// @ts-ignore
|
||||||
if (currentNode.value) {
|
if (currentNode.value) {
|
||||||
const placesToFill = findPlacesToFillInString(currentNode.value)
|
// @ts-ignore
|
||||||
|
const placesToFill = findPlacesToFillInString(currentNode.value, compartment)
|
||||||
if(placesToFill){
|
if(placesToFill){
|
||||||
currentNode.value = placesToFill.fill(data)
|
// @ts-ignore
|
||||||
|
currentNode.value = placesToFill.fill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -454,7 +445,11 @@ export default async function fillOdtTemplate(odtTemplate, data) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const contentXml = await entry.getData(new TextWriter());
|
const contentXml = await entry.getData(new TextWriter());
|
||||||
const contentDocument = parseXML(contentXml);
|
const contentDocument = parseXML(contentXml);
|
||||||
fillTemplatedOdtElement(contentDocument, data, Node)
|
|
||||||
|
const compartment = new Compartment(data)
|
||||||
|
|
||||||
|
fillTemplatedOdtElement(contentDocument, compartment)
|
||||||
|
|
||||||
const updatedContentXml = serializeToString(contentDocument)
|
const updatedContentXml = serializeToString(contentDocument)
|
||||||
|
|
||||||
content = new TextReader(updatedContentXml)
|
content = new TextReader(updatedContentXml)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ Bonjoir ☀️
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('basic template filling with {#if}', async t => {
|
test.skip('basic template filling with {#if}', async t => {
|
||||||
const templatePath = join(import.meta.dirname, './fixtures/description-nombre.odt')
|
const templatePath = join(import.meta.dirname, './fixtures/description-nombre.odt')
|
||||||
const templateContent = `Description du nombre {n}
|
const templateContent = `Description du nombre {n}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ n est un petit nombre
|
|||||||
const templateTextContent = await getOdtTextContent(odtTemplate)
|
const templateTextContent = await getOdtTextContent(odtTemplate)
|
||||||
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template')
|
||||||
|
|
||||||
|
// then branch
|
||||||
const odtResult3 = await fillOdtTemplate(odtTemplate, {n: 3})
|
const odtResult3 = await fillOdtTemplate(odtTemplate, {n: 3})
|
||||||
const odtResult3TextContent = await getOdtTextContent(odtResult3)
|
const odtResult3TextContent = await getOdtTextContent(odtResult3)
|
||||||
t.deepEqual(odtResult3TextContent, `Description du nombre 3
|
t.deepEqual(odtResult3TextContent, `Description du nombre 3
|
||||||
@ -59,6 +59,7 @@ n est un petit nombre
|
|||||||
n est un petit nombre
|
n est un petit nombre
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// else branch
|
||||||
const odtResult8 = await fillOdtTemplate(odtTemplate, {n: 8})
|
const odtResult8 = await fillOdtTemplate(odtTemplate, {n: 8})
|
||||||
const odtResult8TextContent = await getOdtTextContent(odtResult8)
|
const odtResult8TextContent = await getOdtTextContent(odtResult8)
|
||||||
t.deepEqual(odtResult8TextContent, `Description du nombre 8
|
t.deepEqual(odtResult8TextContent, `Description du nombre 8
|
||||||
@ -338,7 +339,6 @@ Les nombres : 1 1 2 3 5 8 13 21 !!
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test('template filling of a table', async t => {
|
test('template filling of a table', async t => {
|
||||||
const templatePath = join(import.meta.dirname, './fixtures/tableau-simple.odt')
|
const templatePath = join(import.meta.dirname, './fixtures/tableau-simple.odt')
|
||||||
const templateContent = `Évolution énergie en kWh par personne en France
|
const templateContent = `Évolution énergie en kWh par personne en France
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user