@jinntec/fore 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/fore-dev.js +2 -2
- package/dist/fore-dev.js.map +1 -1
- package/dist/fore.js +2 -2
- package/dist/fore.js.map +1 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/functions/fx-function.js +4 -95
- package/src/functions/fx-functionlib.js +59 -0
- package/src/functions/registerFunction.js +103 -0
- package/src/fx-fore.js +10 -12
- package/src/fx-instance.js +3 -3
- package/src/fx-model.js +3 -0
- package/src/fx-var.js +7 -7
- package/src/ui/abstract-control.js +3 -1
- package/src/xpath-evaluation.js +2 -1
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { registerCustomXPathFunction, createTypedValueFactory } from 'fontoxpath';
|
|
2
1
|
import { foreElementMixin } from '../ForeElementMixin.js';
|
|
3
|
-
import
|
|
2
|
+
import registerFunction from './registerFunction.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Allows to extend a form with local custom functions.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* @extends {HTMLElement}
|
|
8
|
+
*/
|
|
9
9
|
export class FxFunction extends foreElementMixin(HTMLElement) {
|
|
10
10
|
constructor() {
|
|
11
11
|
super();
|
|
@@ -16,104 +16,13 @@ export class FxFunction extends foreElementMixin(HTMLElement) {
|
|
|
16
16
|
this.style.display = 'none';
|
|
17
17
|
|
|
18
18
|
this.signature = this.hasAttribute('signature') ? this.getAttribute('signature') : null;
|
|
19
|
-
if (this.signature === null) {
|
|
20
|
-
console.error('signature is a required attribute');
|
|
21
|
-
}
|
|
22
19
|
this.type = this.hasAttribute('type') ? this.getAttribute('type') : null;
|
|
23
20
|
this.shadowRoot.innerHTML = `<slot></slot>`;
|
|
24
21
|
|
|
25
22
|
this.override = this.hasAttribute('override') ? this.getAttribute('override') : 'true';
|
|
26
23
|
this.functionBody = this.innerText;
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Parse the signature to something useful
|
|
31
|
-
// Signature is of the form `my:sumproduct($p as xs:decimal*, $q as xs:decimal*) as xs:decimal` or local:something($a as item()*) as item()*
|
|
32
|
-
const signatureParseResult = this.signature.match(
|
|
33
|
-
/(?:(?<prefix>[^:]*):)?(?<localName>[^(]+)\((?<params>(?:\(\)|[^)])*)\)(?: as (?<returnType>.*))?/,
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
if (!signatureParseResult) {
|
|
37
|
-
throw new Error(`Function signature ${this.signature} could not be parsed`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const { prefix, localName, params, returnType } = signatureParseResult.groups;
|
|
41
|
-
|
|
42
|
-
// TODO: lookup prefix
|
|
43
|
-
const functionIdentifier =
|
|
44
|
-
prefix === 'local' || !prefix
|
|
45
|
-
? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
|
|
46
|
-
: `${prefix}:${localName}`;
|
|
47
|
-
|
|
48
|
-
// Make the function available globally w/o a prefix. See the functionNameResolver for for how
|
|
49
|
-
// this is picked up
|
|
50
|
-
if (!prefix) {
|
|
51
|
-
globallyDeclaredFunctionLocalNames.push(localName);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const paramParts = params
|
|
55
|
-
? params.split(',').map(param => {
|
|
56
|
-
const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>.+))/);
|
|
57
|
-
if (!match) {
|
|
58
|
-
throw new Error(`Param ${param} could not be parsed`);
|
|
59
|
-
}
|
|
60
|
-
const { variableName, varType } = match.groups;
|
|
61
|
-
return {
|
|
62
|
-
variableName,
|
|
63
|
-
variableType: varType || 'item()*',
|
|
64
|
-
};
|
|
65
|
-
})
|
|
66
|
-
: [];
|
|
67
|
-
|
|
68
|
-
switch (type) {
|
|
69
|
-
case 'text/javascript': {
|
|
70
|
-
// eslint-disable-next-line no-new-func
|
|
71
|
-
const fun = new Function(
|
|
72
|
-
'_domFacade',
|
|
73
|
-
...paramParts.map(paramPart => paramPart.variableName),
|
|
74
|
-
'form',
|
|
75
|
-
this.functionBody,
|
|
76
|
-
);
|
|
77
|
-
registerCustomXPathFunction(
|
|
78
|
-
functionIdentifier,
|
|
79
|
-
paramParts.map(paramPart => paramPart.variableType),
|
|
80
|
-
returnType || 'item()*',
|
|
81
|
-
(...args) => fun.apply(this.getInScopeContext(), [...args, this.getOwnerForm()]),
|
|
82
|
-
);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'text/xquf':
|
|
87
|
-
case 'text/xquery':
|
|
88
|
-
case 'text/xpath': {
|
|
89
|
-
const typedValueFactories = paramParts.map(param => createTypedValueFactory(param.variableType));
|
|
90
|
-
const language = type === 'text/xpath' ?
|
|
91
|
-
'XPath3.1' : type === 'text/xquery' ?
|
|
92
|
-
'XQuery3.1' : 'XQueryUpdate3.1';
|
|
93
|
-
const fun = (domFacade, ...args) =>
|
|
94
|
-
evaluateXPath(
|
|
95
|
-
this.functionBody,
|
|
96
|
-
this.getInScopeContext(),
|
|
97
|
-
this.getOwnerForm(),
|
|
98
|
-
paramParts.reduce((variablesByName, paramPart, i) => {
|
|
99
|
-
// Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
|
|
100
|
-
variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](args[i]);
|
|
101
|
-
return variablesByName;
|
|
102
|
-
}, {}),
|
|
103
|
-
{language}
|
|
104
|
-
);
|
|
105
|
-
registerCustomXPathFunction(
|
|
106
|
-
functionIdentifier,
|
|
107
|
-
paramParts.map(paramPart => paramPart.variableType),
|
|
108
|
-
returnType || 'item()*',
|
|
109
|
-
fun,
|
|
110
|
-
);
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
default:
|
|
115
|
-
throw new Error(`Unexpected mimetype ${type} for function`);
|
|
116
|
-
}
|
|
25
|
+
registerFunction(this, this);
|
|
117
26
|
}
|
|
118
27
|
}
|
|
119
28
|
if (!customElements.get('fx-function')) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { foreElementMixin } from '../ForeElementMixin.js';
|
|
2
|
+
import registerFunction from './registerFunction.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Allows to extend a form with remote custom functions.
|
|
6
|
+
*
|
|
7
|
+
* @extends {HTMLElement}
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class FxFunctionlib extends foreElementMixin(HTMLElement) {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @type {Function}
|
|
16
|
+
*/
|
|
17
|
+
this._resolve = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @type {Promise<undefined>}
|
|
21
|
+
*/
|
|
22
|
+
this.readyPromise = new Promise((resolve) => this._resolveLoading = resolve);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async connectedCallback() {
|
|
26
|
+
this.style.display = 'none';
|
|
27
|
+
|
|
28
|
+
const src = this.getAttribute('src');
|
|
29
|
+
|
|
30
|
+
const result = await fetch(src);
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
console.error(`Loading function library at ${src} failed.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const body = await result.text();
|
|
36
|
+
const document = (new DOMParser()).parseFromString(body, 'text/html');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @type {HTMLElement[]}
|
|
40
|
+
*/
|
|
41
|
+
const functions = Array.from(document.querySelectorAll('fx-function'));
|
|
42
|
+
// TODO: also recurse into new function libraries here?
|
|
43
|
+
for (const func of functions) {
|
|
44
|
+
const functionObject = {
|
|
45
|
+
type: func.getAttribute('type'),
|
|
46
|
+
signature: func.getAttribute('signature'),
|
|
47
|
+
functionBody: func.innerText
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
registerFunction(functionObject, this);
|
|
51
|
+
}
|
|
52
|
+
this._resolveLoading(undefined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if (!customElements.get('fx-functionlib')) {
|
|
58
|
+
customElements.define('fx-functionlib', FxFunctionlib);
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createTypedValueFactory, registerCustomXPathFunction } from "fontoxpath";
|
|
2
|
+
import { evaluateXPath, globallyDeclaredFunctionLocalNames } from "../xpath-evaluation";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param functionObject {{signature: string, type: string|null, functionBody: string}}
|
|
6
|
+
* @param formElement {HTMLElement} The form element connected to this function. Used to determine inscope context
|
|
7
|
+
* @returns {undefined}
|
|
8
|
+
*/
|
|
9
|
+
export default function (functionObject, formElement) {
|
|
10
|
+
if (functionObject.signature === null) {
|
|
11
|
+
console.error('signature is a required attribute');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const type = functionObject.type ?? 'text/xpath';
|
|
15
|
+
|
|
16
|
+
// Parse the signature to something useful
|
|
17
|
+
// Signature is of the form `my:sumproduct($p as xs:decimal*, $q as xs:decimal*) as xs:decimal` or local:something($a as item()*) as item()*
|
|
18
|
+
const signatureParseResult = functionObject.signature.match(
|
|
19
|
+
/(?:(?<prefix>[^:]*):)?(?<localName>[^(]+)\((?<params>(?:\(\)|[^)])*)\)(?: as (?<returnType>.*))?/,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!signatureParseResult) {
|
|
23
|
+
throw new Error(`Function signature ${functionObject.signature} could not be parsed`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { prefix, localName, params, returnType } = signatureParseResult.groups;
|
|
27
|
+
|
|
28
|
+
// TODO: lookup prefix
|
|
29
|
+
const functionIdentifier =
|
|
30
|
+
prefix === 'local' || !prefix
|
|
31
|
+
? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
|
|
32
|
+
: `${prefix}:${localName}`;
|
|
33
|
+
|
|
34
|
+
// Make the function available globally w/o a prefix. See the functionNameResolver for for how
|
|
35
|
+
// functionObject is picked up
|
|
36
|
+
if (!prefix) {
|
|
37
|
+
globallyDeclaredFunctionLocalNames.push(localName);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const paramParts = params
|
|
41
|
+
? params.split(',').map(param => {
|
|
42
|
+
const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>[^\s]+))/);
|
|
43
|
+
if (!match) {
|
|
44
|
+
throw new Error(`Param ${param} could not be parsed`);
|
|
45
|
+
}
|
|
46
|
+
const { variableName, varType } = match.groups;
|
|
47
|
+
return {
|
|
48
|
+
variableName,
|
|
49
|
+
variableType: varType || 'item()*',
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
switch (type) {
|
|
55
|
+
case 'text/javascript': {
|
|
56
|
+
// eslint-disable-next-line no-new-func
|
|
57
|
+
const fun = new Function(
|
|
58
|
+
'_domFacade',
|
|
59
|
+
...paramParts.map(paramPart => paramPart.variableName),
|
|
60
|
+
'form',
|
|
61
|
+
functionObject.functionBody,
|
|
62
|
+
);
|
|
63
|
+
registerCustomXPathFunction(
|
|
64
|
+
functionIdentifier,
|
|
65
|
+
paramParts.map(paramPart => paramPart.variableType),
|
|
66
|
+
returnType || 'item()*',
|
|
67
|
+
(...args) => fun.apply(formElement.getInScopeContext(), [...args, formElement.getOwnerForm()]),
|
|
68
|
+
);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'text/xquf':
|
|
73
|
+
case 'text/xquery':
|
|
74
|
+
case 'text/xpath': {
|
|
75
|
+
const typedValueFactories = paramParts.map(param => createTypedValueFactory(param.variableType));
|
|
76
|
+
const language = type === 'text/xpath' ?
|
|
77
|
+
'XPath3.1' : type === 'text/xquery' ?
|
|
78
|
+
'XQuery3.1' : 'XQueryUpdate3.1';
|
|
79
|
+
const fun = (domFacade, ...args) =>
|
|
80
|
+
evaluateXPath(
|
|
81
|
+
functionObject.functionBody,
|
|
82
|
+
formElement.getInScopeContext(),
|
|
83
|
+
formElement.getOwnerForm(),
|
|
84
|
+
paramParts.reduce((variablesByName, paramPart, i) => {
|
|
85
|
+
// Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
|
|
86
|
+
variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](args[i], domFacade);
|
|
87
|
+
return variablesByName;
|
|
88
|
+
}, {}),
|
|
89
|
+
{language}
|
|
90
|
+
);
|
|
91
|
+
registerCustomXPathFunction(
|
|
92
|
+
functionIdentifier,
|
|
93
|
+
paramParts.map(paramPart => paramPart.variableType),
|
|
94
|
+
returnType || 'item()*',
|
|
95
|
+
fun,
|
|
96
|
+
);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
throw new Error(`Unexpected mimetype ${type} for function`);
|
|
102
|
+
}
|
|
103
|
+
};
|
package/src/fx-fore.js
CHANGED
|
@@ -291,6 +291,16 @@ export class FxFore extends HTMLElement {
|
|
|
291
291
|
"background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;",
|
|
292
292
|
);
|
|
293
293
|
|
|
294
|
+
const variables = new Map();
|
|
295
|
+
(function registerVariables(node) {
|
|
296
|
+
for (const child of node.children) {
|
|
297
|
+
if ('setInScopeVariables' in child) {
|
|
298
|
+
child.setInScopeVariables(variables);
|
|
299
|
+
}
|
|
300
|
+
registerVariables(child);
|
|
301
|
+
}
|
|
302
|
+
})(this);
|
|
303
|
+
|
|
294
304
|
await modelElement.modelConstruct();
|
|
295
305
|
this._handleModelConstructDone();
|
|
296
306
|
}
|
|
@@ -900,18 +910,6 @@ export class FxFore extends HTMLElement {
|
|
|
900
910
|
this.classList.add('initialRun');
|
|
901
911
|
await this._lazyCreateInstance();
|
|
902
912
|
|
|
903
|
-
// console.log('registering variables!');
|
|
904
|
-
const variables = new Map();
|
|
905
|
-
(function registerVariables(node) {
|
|
906
|
-
for (const child of node.children) {
|
|
907
|
-
if ('setInScopeVariables' in child) {
|
|
908
|
-
child.setInScopeVariables(variables);
|
|
909
|
-
}
|
|
910
|
-
registerVariables(child);
|
|
911
|
-
}
|
|
912
|
-
})(this);
|
|
913
|
-
// console.log('Found variables:', variables);
|
|
914
|
-
|
|
915
913
|
/*
|
|
916
914
|
const options = {
|
|
917
915
|
root: null,
|
package/src/fx-instance.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Fore } from './fore.js';
|
|
2
2
|
import { evaluateXPathToFirstNode } from './xpath-evaluation.js';
|
|
3
3
|
|
|
4
|
-
async function handleResponse(response) {
|
|
4
|
+
async function handleResponse(fxInstance, response) {
|
|
5
5
|
const { status } = response;
|
|
6
6
|
if (status >= 400) {
|
|
7
7
|
// console.log('response status', status);
|
|
8
|
-
alert(`response status: ${status} - failed to load data for '${
|
|
8
|
+
alert(`response status: ${status} - failed to load data for '${fxInstance.src}' - stopping.`);
|
|
9
9
|
throw new Error(`failed to load data - status: ${status}`);
|
|
10
10
|
}
|
|
11
11
|
const responseContentType = response.headers.get('content-type').toLowerCase();
|
|
@@ -258,7 +258,7 @@ export class FxInstance extends HTMLElement {
|
|
|
258
258
|
'Content-Type': contentType,
|
|
259
259
|
},
|
|
260
260
|
});
|
|
261
|
-
|
|
261
|
+
const data = await handleResponse(this, response);
|
|
262
262
|
this._setInitialData(data);
|
|
263
263
|
/*
|
|
264
264
|
if (data.nodeType) {
|
package/src/fx-model.js
CHANGED
|
@@ -138,6 +138,9 @@ export class FxModel extends HTMLElement {
|
|
|
138
138
|
}),
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
const functionlibImports = Array.from(this.querySelectorAll('fx-functionlib'));
|
|
143
|
+
await Promise.all(functionlibImports.map(lib => lib.readyPromise));
|
|
141
144
|
// console.timeEnd('instance-loading');
|
|
142
145
|
this.inited = true;
|
|
143
146
|
}
|
package/src/fx-var.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { createTypedValueFactory, domFacade } from 'fontoxpath';
|
|
1
2
|
import { Fore } from './fore.js';
|
|
2
3
|
import './fx-instance.js';
|
|
3
4
|
import { evaluateXPath } from './xpath-evaluation.js';
|
|
4
5
|
import { foreElementMixin } from './ForeElementMixin.js';
|
|
5
6
|
import getInScopeContext from './getInScopeContext.js';
|
|
6
7
|
|
|
8
|
+
// We are getting sequences here (evaluateXPath is returning all items, as an array)
|
|
9
|
+
// So wrap them into something so FontoXPath also understands they are sequences, always.
|
|
10
|
+
const typedValueFactory = createTypedValueFactory('item()*');
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* @ts-check
|
|
9
14
|
*/
|
|
@@ -25,13 +30,8 @@ export class FxVariable extends foreElementMixin(HTMLElement) {
|
|
|
25
30
|
refresh() {
|
|
26
31
|
const inscope = getInScopeContext(this, this.valueQuery);
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
[this.value] = values;
|
|
31
|
-
} else {
|
|
32
|
-
// There is no value: set to null so it's interpreted as empty-sequence later on
|
|
33
|
-
this.value = null;
|
|
34
|
-
}
|
|
33
|
+
const values = evaluateXPath(this.valueQuery, inscope, this, this.precedingVariables);
|
|
34
|
+
this.value = typedValueFactory(values, domFacade);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
setInScopeVariables(inScopeVariables) {
|
|
@@ -144,7 +144,9 @@ export default class AbstractControl extends foreElementMixin(HTMLElement) {
|
|
|
144
144
|
Fore.dispatch(this,'init',{});
|
|
145
145
|
}
|
|
146
146
|
if (!this.getOwnerForm().ready) return; // state change event do not fire during init phase (initial refresh)
|
|
147
|
-
|
|
147
|
+
// if oldVal is null we haven't received a concrete value yet
|
|
148
|
+
|
|
149
|
+
if (this.localName !== 'fx-control') return;
|
|
148
150
|
if (this.oldVal !== null && currentVal !== this.value) {
|
|
149
151
|
Fore.dispatch(this, 'value-changed', { path: this.modelItem.path , value:this.modelItem.value});
|
|
150
152
|
}
|
package/src/xpath-evaluation.js
CHANGED