From f735118ec5c76c0c17fbf11afbccaeedb6b6441e Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Fri, 27 Aug 2021 13:09:52 -0700 Subject: [PATCH] Uplift OpenrosaXpathEvaluatorBinding --- webapp/custom-webpack.config.js | 4 +- .../enketo/OpenrosaXpathEvaluatorBinding.js | 68 +++++++++++++------ webapp/src/js/enketo/widgets.js | 2 +- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/webapp/custom-webpack.config.js b/webapp/custom-webpack.config.js index 3af11472f94..4660b502e1b 100644 --- a/webapp/custom-webpack.config.js +++ b/webapp/custom-webpack.config.js @@ -4,7 +4,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl module.exports = { resolve: { alias: { - // TODO Do we also need mapping for enketo/translator, enketo/dialog? Probably only if we have custom impls. + // TODO Do we also need mapping for enketo/dialog? Probably only if we have custom impls. 'enketo/config': 'src/js/enketo/config.js', 'enketo/widgets': 'src/js/enketo/widgets', 'enketo/xpath-evaluator-binding': 'src/js/enketo/OpenrosaXpathEvaluatorBinding', @@ -12,7 +12,7 @@ module.exports = { 'extended-xpath': 'node_modules/openrosa-xpath-evaluator/src/extended-xpath', 'openrosa-extensions': 'node_modules/openrosa-xpath-evaluator/src/openrosa-extensions', // translator for enketo's internal i18n - 'translator': 'src/js/enketo/translator', + 'enketo/translator': 'src/js/enketo/translator', // enketo currently duplicates bootstrap's dropdown code. working to resolve this upstream // https://github.com/enketo/enketo-core/issues/454 '../../js/dropdown.jquery': 'node_modules/bootstrap/js/dropdown', diff --git a/webapp/src/js/enketo/OpenrosaXpathEvaluatorBinding.js b/webapp/src/js/enketo/OpenrosaXpathEvaluatorBinding.js index 3e158d2bd12..85c8d9a0708 100644 --- a/webapp/src/js/enketo/OpenrosaXpathEvaluatorBinding.js +++ b/webapp/src/js/enketo/OpenrosaXpathEvaluatorBinding.js @@ -1,27 +1,51 @@ -const ExtendedXpathEvaluator = require('extended-xpath'); +const ExtendedXPathEvaluator = require('extended-xpath'); const openrosaExtensions = require('openrosa-extensions'); const medicExtensions = require('./medic-xpath-extensions'); -const translator = require('./translator'); +const XPR = require('node_modules/openrosa-xpath-evaluator/src/xpr'); +const {asString, asBoolean, asNumber} = require('node_modules/openrosa-xpath-evaluator/src/utils/xpath-cast'); -module.exports = function() { - // re-implement XPathJS ourselves! - const evaluator = new XPathEvaluator(); - this.xml.jsCreateExpression = function() { - return evaluator.createExpression.apply( evaluator, arguments ); - }; - this.xml.jsCreateNSResolver = function() { - return evaluator.createNSResolver.apply( evaluator, arguments ); - }; - this.xml.jsEvaluate = function(e, contextPath, namespaceResolver, resultType, result) { - const extensions = openrosaExtensions(translator.t); - extensions.func = Object.assign(extensions.func, medicExtensions.func); - extensions.process = Object.assign(extensions.process, medicExtensions.process); - const evaluator = new ExtendedXpathEvaluator(contextPath.ownerDocument, extensions); - return evaluator.evaluate(e, contextPath, namespaceResolver, resultType, result); +const cast = { + string: asString, + boolean: asBoolean, + number: asNumber, +}; + +module.exports = function( ) { + const ore = openrosaExtensions(); + ore.func = Object.assign(ore.func, medicExtensions.func); + ore.process = Object.assign(ore.process, medicExtensions.process); + const evaluator = new ExtendedXPathEvaluator(new XPathEvaluator(), ore); + + evaluator.customXPathFunction = { + add: (name, { fn, args:_args, ret }) => { + if(Object.prototype.hasOwnProperty.call(ore.func, name)) { + throw new Error(`There is already a function with the name: '${name}'`); + } + + const argTypes = _args.map(a => a.t); + const allowedArgTypes = Object.keys(cast); + const unsupportedArgTypes = argTypes.filter(t => !allowedArgTypes.includes(t)); + if(unsupportedArgTypes.length) { + const quoted = unsupportedArgTypes.map(t => `'${t}'`); + throw new Error(`Unsupported arg type(s): ${quoted.join(',')}`); + } + + const allowedRetTypes = Object.keys(XPR); + if(!allowedRetTypes.includes(ret)) { + throw new Error(`Unsupported return type: '${ret}'`); + } + + ore.func[name] = (...args) => { + if(args.length !== argTypes.length) { + throw new Error(`Function "${name}" expected ${argTypes.length} arg(s), but got ${args.length}`); + } + + const convertedArgs = argTypes.map((type, idx) => cast[type](args[idx])); + return XPR[ret](fn(...convertedArgs)); + }; + }, }; - window.JsXPathException = - window.JsXPathExpression = - window.JsXPathNSResolver = - window.JsXPathResult = - window.JsXPathNamespace = true; + + this.xml.jsEvaluate = evaluator.evaluate; + return evaluator; }; diff --git a/webapp/src/js/enketo/widgets.js b/webapp/src/js/enketo/widgets.js index 2ff4bb3a50e..fd865fb5ae9 100644 --- a/webapp/src/js/enketo/widgets.js +++ b/webapp/src/js/enketo/widgets.js @@ -3,8 +3,8 @@ require( 'enketo-core/src/widget/geo/geopicker' ), require( 'enketo-core/src/widget/table/tablewidget' ), require( 'enketo-core/src/widget/radio/radiopicker' ), - require( 'enketo-core/src/widget/date/datepicker-extended' ), require( 'enketo-core/src/widget/time/timepicker-extended' ), + require( 'enketo-core/src/widget/columns/columns' ), require( 'enketo-core/src/widget/file/filepicker' ), require( './widgets/datepicker-widget' ), require( './widgets/horizontal-choices' ),