@jinntec/fore 2.7.2 → 2.9.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/dist/fore-dev.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Version: 2.7.2 - December 9, 2025 15:22:48 */
1
+ /* Version: 2.9.0 - January 21, 2026 15:42:06 */
2
2
  function t$2(t, s, r, i) {
3
3
  const n = {
4
4
  op: s,
@@ -16822,1978 +16822,1979 @@ class DependencyNotifyingDomFacade {
16822
16822
  }
16823
16823
  }
16824
16824
 
16825
- function prettifyXml(source) {
16826
- const xmlDoc = new DOMParser().parseFromString(source, 'application/xml');
16827
- const xsltDoc = new DOMParser().parseFromString([
16828
- // describes how we want to modify the XML - indent everything
16829
- '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">', ' <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>', ' <xsl:strip-space elements="*"/>', ' <xsl:template match="text()">',
16830
- // change to just text() to strip space in text nodes
16831
- ' <xsl:value-of select="normalize-space(.)"/>', ' </xsl:template>', ' <xsl:template match="node()|@*">', ' <xsl:copy>', ' <xsl:apply-templates select="node()|@*"/>', ' </xsl:copy>', ' </xsl:template>', '</xsl:stylesheet>'].join('\n'), 'application/xml');
16832
- const xsltProcessor = new XSLTProcessor();
16833
- xsltProcessor.importStylesheet(xsltDoc);
16834
- const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
16835
- const resultXml = new XMLSerializer().serializeToString(resultDoc);
16836
- return resultXml;
16837
- }
16838
-
16839
- const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
16840
- const createdNamespaceResolversByXPathQueryAndNode = new Map();
16841
-
16842
- // A global registry of function names that are declared in Fore by a developer using the
16843
- // `fx-function` element. These should be available without providing a prefix as well
16844
- const globallyDeclaredFunctionLocalNames = [];
16845
- function getCachedNamespaceResolver(xpath, node) {
16846
- if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
16847
- return null;
16848
- }
16849
- return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
16850
- }
16851
- function setCachedNamespaceResolver(xpath, node, resolver) {
16852
- if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
16853
- return createdNamespaceResolversByXPathQueryAndNode.set(xpath, new Map());
16854
- }
16855
- return createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
16856
- }
16857
- const xhtmlNamespaceResolver = prefix => {
16858
- if (!prefix) {
16859
- return 'http://www.w3.org/1999/xhtml';
16860
- }
16861
- return undefined;
16862
- };
16825
+ class XPathUtil {
16826
+ /**
16827
+ * Recursively check AST for any dynamic expression components.
16828
+ */
16829
+ static containsDynamicContent(astNode) {
16830
+ if (!astNode) return false;
16863
16831
 
16864
- /**
16865
- * Resolve an id in scope. Behaves like the algorithm defined on https://www.w3.org/community/xformsusers/wiki/XForms_2.0#idref-resolve
16866
- *
16867
- * @param {string} id
16868
- * @param {Node} sourceObject
16869
- * @param {string} nodeName
16870
- *
16871
- * @returns {HTMLElement} The element with that ID, resolved with respect to repeats
16872
- */
16873
- function resolveId(id, sourceObject, nodeName = null) {
16874
- const query = 'outermost(ancestor-or-self::fx-fore[1]/(descendant::fx-fore|descendant::*[@id = $id]))[not(self::fx-fore)]';
16875
- /*
16876
- if (nodeName === 'fx-instance') {
16877
- // Instance elements can only be in the `model` element
16878
- // query = 'ancestor-or-self::fx-fore[1]/fx-model/fx-instance[@id = $id]';
16879
- const fore = Fore.getFore(sourceObject);
16880
- const instances = fore.getModel().instances;
16881
- const targetInstance = instances.find(i => i.id === id);
16882
- return targetInstance;
16883
- return document.getElementById(id);
16884
- }
16885
- */
16886
- if (sourceObject.nodeType === Node.TEXT_NODE) {
16887
- sourceObject = sourceObject.parentNode;
16888
- }
16889
- if (sourceObject.nodeType === Node.ATTRIBUTE_NODE) {
16890
- sourceObject = sourceObject.ownerElement;
16891
- }
16892
- if (sourceObject.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
16893
- sourceObject = sourceObject.parentNode.host;
16894
- }
16895
- const ownerForm = sourceObject.localName === 'fx-fore' ? sourceObject : sourceObject.closest('fx-fore');
16896
- const elementsWithId = ownerForm.querySelectorAll(`[id='${id}']`);
16897
- if (elementsWithId.length === 1) {
16898
- // A single one is found. Assume no ID reuse.
16899
- const targetObject = elementsWithId[0];
16900
- if (nodeName && targetObject.localName !== nodeName) {
16901
- return null;
16832
+ // Location paths, function calls, or variable refs are dynamic
16833
+ if (astNode.type === 'pathExpression' || astNode.type === 'functionCall' || astNode.type === 'variableReference') {
16834
+ return true;
16902
16835
  }
16903
- return targetObject;
16904
- }
16905
- const allMatchingTargetObjects = evaluateXPathToNodes$1(query, sourceObject, null, {
16906
- id
16907
- }, {
16908
- namespaceResolver: xhtmlNamespaceResolver
16909
- });
16910
- if (allMatchingTargetObjects.length === 0) {
16911
- return null;
16912
- }
16913
- if (allMatchingTargetObjects.length === 1 && evaluateXPathToBoolean$1('(ancestor::fx-fore | ancestor::fx-repeat)[last()]/self::fx-fore', allMatchingTargetObjects[0], null, null, {
16914
- namespaceResolver: xhtmlNamespaceResolver
16915
- })) {
16916
- // If the target element is not repeated, then the search for the target object is trivial since
16917
- // there is only one associated with the target element that bears the matching ID. This is true
16918
- // regardless of whether or not the source object is repeated. However, if the target element is
16919
- // repeated, then additional information must be used to help select a target object from among
16920
- // those associated with the identified target element.
16921
- const targetObject = allMatchingTargetObjects[0];
16922
- if (nodeName && targetObject.localName !== nodeName) {
16923
- return null;
16836
+
16837
+ // Recursively check any child expressions
16838
+ for (const key in astNode) {
16839
+ if (astNode[key] && typeof astNode[key] === 'object') {
16840
+ if (Array.isArray(astNode[key])) {
16841
+ for (const item of astNode[key]) {
16842
+ if (XPathUtil.containsDynamicContent(item)) return true;
16843
+ }
16844
+ } else {
16845
+ if (XPathUtil.containsDynamicContent(astNode[key])) return true;
16846
+ }
16847
+ }
16924
16848
  }
16925
- return targetObject;
16849
+ return false;
16926
16850
  }
16927
16851
 
16928
- // SPEC:
16852
+ /**
16853
+ * creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
16854
+ * nodes.
16855
+ * E.g. 'foo/bar' creates an element 'foo' with an child element 'bar'
16856
+ * 'foo/@bar' creates a 'foo' element with an 'bar' attribute
16857
+ *
16858
+ * supports multiple steps
16859
+ *
16860
+ * @param xpath
16861
+ * @param doc {XMLDocument}
16862
+ * @param fore
16863
+ * @param namespaceResolver {function} optional namespace resolver function
16864
+ * @return {Node|Attr}
16865
+ */
16866
+ static createNodesFromXPath(xpath, doc, fore, namespaceResolver = null) {
16867
+ const resolveNamespace = namespaceResolver || (() => undefined);
16868
+ if (!doc) {
16869
+ doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
16870
+ }
16929
16871
 
16930
- // 12.2.1 References to Elements within a repeat Element
16872
+ const parts = [];
16873
+ let scratch = '';
16874
+ let isInPredicate = false;
16875
+ for (const char of xpath.split('')) {
16876
+ if (!isInPredicate) {
16877
+ // We are not in a predicate, the slash will terminate our step.
16878
+ if (char === '/') {
16879
+ parts.push(scratch);
16880
+ scratch = '';
16881
+ continue;
16882
+ }
16883
+ scratch += char;
16884
+ if (char === '[') {
16885
+ isInPredicate = true;
16886
+ }
16887
+ continue;
16888
+ }
16889
+ // We are in a predicate! So the only interesting token is ']', which means we're out of one.
16890
+ scratch += char;
16891
+ if (char === ']') {
16892
+ isInPredicate = false;
16893
+ }
16894
+ }
16895
+ // Flush the last step
16896
+ parts.push(scratch);
16897
+ let rootNode = null;
16898
+ let currentNode = null;
16899
+ for (const part of parts) {
16900
+ if (!part) continue; // Skip empty parts (e.g., leading slashes)
16901
+ if (part === '.') {
16902
+ // A '.' does not introduce new elements
16903
+ continue;
16904
+ }
16931
16905
 
16932
- // When the target element that is identified by the IDREF of a source object has one or more
16933
- // repeat elements as ancestors, then the set of ancestor repeats are partitioned into two
16934
- // subsets, those in common with the source element and those that are not in common. Any ancestor
16935
- // repeat elements of the target element not in common with the source element are descendants of
16936
- // the repeat elements that the source and target element have in common, if any.
16906
+ // Handle attributes
16907
+ if (part.startsWith('@')) {
16908
+ const attrName = part.slice(1); // Strip '@'
16909
+ if (!currentNode) {
16910
+ return doc.createAttribute(attrName, '');
16911
+ }
16912
+ currentNode.setAttribute(attrName, '');
16913
+ } else {
16914
+ // We are a predicate selector! Handle it
16915
+ // This regex matches strings like:
16916
+ // - listBibl
16917
+ // - tei:listBibl
16918
+ // - listBibl[@type="foo"]
16919
+ // - listBibl[@type="foo"][@class="bar"]
16920
+ // It will also match strings like
16921
+ // - listBibl[ancestor-or-self::foo]
16922
+ // which will be filtered out later.
16937
16923
 
16938
- // For the repeat elements that are in common, the desired target object exists in the same set of
16939
- // run-time objects that contains the source object. Then, for each ancestor repeat of the target
16940
- // element that is not in common with the source element, the current index of the repeat
16941
- // determines the set of run-time objects that contains the desired target object.
16942
- for (const ancestorRepeatItem of evaluateXPathToNodes$1('ancestor::fx-repeatitem => reverse()', sourceObject, null, null, {
16943
- namespaceResolver: xhtmlNamespaceResolver
16944
- })) {
16945
- const foundTargetObjects = allMatchingTargetObjects.filter(to => XPathUtil.contains(ancestorRepeatItem, to));
16946
- switch (foundTargetObjects.length) {
16947
- case 0:
16948
- // Nothing found: ignore
16949
- break;
16950
- case 1:
16951
- {
16952
- // A single one is found: the target object is directly in a common repeat
16953
- const targetObject = foundTargetObjects[0];
16954
- if (nodeName && targetObject.localName !== nodeName) {
16955
- return null;
16956
- }
16957
- return targetObject;
16924
+ const result = part.match(/^(?<name>[\w:-]+)(?<predicates>(\[[^]*\])*)$/);
16925
+ if (!result) {
16926
+ throw new Error(`No element could be made from the XPath step ${part}. It must be of these forms: 'localName', 'prefix:name', 'name[@attr="value"]' et cetera.`);
16958
16927
  }
16959
- default:
16960
- {
16961
- // Multiple target objects are found: they are in a repeat that is not common with the
16962
- // source object We found a target object in a common repeat! We now need to find the one
16963
- // that is in the repeatitem identified at the current index
16964
- const targetObject = foundTargetObjects.find(to => evaluateXPathToNodes$1('every $ancestor of ancestor::fx-repeatitem satisfies $ancestor is $ancestor/../child::fx-repeatitem[../@repeat-index]', to, null, {}));
16965
- if (!targetObject) {
16966
- // Nothing valid found for whatever reason. This might be something dynamic?
16967
- return null;
16968
- }
16969
- if (nodeName && targetObject.localName !== nodeName) {
16970
- return null;
16928
+ const {
16929
+ name,
16930
+ predicates
16931
+ } = result.groups;
16932
+ // Handle namespaces if present
16933
+ const [prefix, localName] = name.includes(':') ? name.split(':') : [null, name];
16934
+ const namespace = resolveNamespace(prefix);
16935
+ const newElement = namespace ? doc.createElementNS(namespace, localName) : doc.createElement(localName);
16936
+ if (predicates) {
16937
+ const predicateExtractionRegex = /(\[@(?<name>[\w:-]*)\s?=\s?["'](?<value>[^"']*)['"]\])+/g;
16938
+ const parsedPredicates = predicates.matchAll(predicateExtractionRegex).map(match => ({
16939
+ attrName: match.groups.name,
16940
+ value: match.groups.value
16941
+ }));
16942
+ for (const {
16943
+ attrName,
16944
+ value
16945
+ } of parsedPredicates) {
16946
+ newElement.setAttribute(attrName, value);
16971
16947
  }
16972
- return targetObject;
16973
16948
  }
16949
+ if (!rootNode) {
16950
+ rootNode = newElement; // Set as the root node
16951
+ } else {
16952
+ currentNode.appendChild(newElement);
16953
+ }
16954
+ currentNode = newElement;
16955
+ }
16956
+ }
16957
+ if (!rootNode) {
16958
+ throw new Error('Invalid XPath; no root element could be created.');
16974
16959
  }
16960
+ return rootNode;
16975
16961
  }
16976
- // We found no target objects in common repeats. The id is unresolvable
16977
- return null;
16978
- }
16979
16962
 
16980
- // Make namespace resolving use the `instance` element that is related to here
16981
- const xmlDocument = new DOMParser().parseFromString('<xml />', 'text/xml');
16982
- const instanceReferencesByQuery = new Map();
16983
- function findInstanceReferences(xpathQuery) {
16984
- if (!xpathQuery.includes('instance')) {
16985
- // No call to the instance function anyway: short-circuit and prevent AST processing
16986
- return [];
16963
+ /**
16964
+ * looks up namespace on ownerForm. Though not strictly in the sense of resolving namespaces in XML, the
16965
+ * fx-fore element is a convenient place to put namespace declarations for 2 reasons:
16966
+ * - this way namespaces are scoped to a Fore element
16967
+ * - as fx-fore is a web component we can add our xmlns attributes as we got no restrictions to attributes
16968
+ * though strictly speaking they are no xmlns declarations and just serve the purpose of namespace lookup.
16969
+ *
16970
+ * @param boundElement
16971
+ * @param prefix
16972
+ * @return {string}
16973
+ */
16974
+ static lookupNamespace(ownerForm, prefix) {
16975
+ return ownerForm.getAttribute(`xmlns:${prefix}`);
16987
16976
  }
16988
- if (instanceReferencesByQuery.has(xpathQuery)) {
16989
- return instanceReferencesByQuery.get(xpathQuery);
16977
+ static querySelectorAll(querySelector, start) {
16978
+ const queue = [start];
16979
+ const found = [];
16980
+ while (queue.length) {
16981
+ const item = queue.shift();
16982
+ for (const child of Array.from(item.children).reverse()) {
16983
+ queue.unshift(child);
16984
+ }
16985
+ if (item.matches && item.matches('template')) {
16986
+ queue.unshift(item.content);
16987
+ }
16988
+ if (item.matches && item.matches(querySelector)) {
16989
+ found.push(item);
16990
+ }
16991
+ }
16992
+ return found;
16990
16993
  }
16991
- const xpathAST = parseScript(xpathQuery, {}, xmlDocument);
16992
- const instanceReferences = evaluateXPathToStrings$1(`descendant::xqx:functionCallExpr
16993
- [xqx:functionName = "instance"]
16994
- /xqx:arguments
16995
- /xqx:stringConstantExpr
16996
- /xqx:value`, xpathAST, null, {}, {
16997
- namespaceResolver: prefix => prefix === 'xqx' ? 'http://www.w3.org/2005/XQueryX' : undefined
16998
- });
16999
- instanceReferencesByQuery.set(xpathQuery, instanceReferences);
17000
- return instanceReferences;
17001
- }
17002
- /**
17003
- * @typedef {function(string):string} NamespaceResolver
17004
- */
17005
16994
 
17006
- /**
17007
- * @function
17008
- * Resolve a namespace. Needs a namespace prefix and the element that is most closely related to the
17009
- * XPath in which the namespace is being resolved. The prefix will be resolved by using the
17010
- * ancestry of said element.
17011
- *
17012
- * It has two ways of doing so:
17013
- *
17014
- * - If the prefix is defined in an `xmlns:XXX="YYY"` namespace declaration, it will return 'YYY'.
17015
- * - If the prefix is the empty prefix and there is an `xpath-default-namespace="YYY"` attribute in
17016
- * - the * ancestry, that attribute will be used and 'YYY' will be returned
17017
- *
17018
- * @param {string} xpathQuery
17019
- * @param {HTMLElement} formElement
17020
- * @returns {NamespaceResolver} The namespace resolver for this context
17021
- */
17022
- function createNamespaceResolver(xpathQuery, formElement) {
17023
- const cachedResolver = getCachedNamespaceResolver(xpathQuery, formElement);
17024
- if (cachedResolver) {
17025
- return cachedResolver;
17026
- }
17027
- let instanceReferences = findInstanceReferences(xpathQuery);
17028
- if (instanceReferences.length === 0) {
17029
- // No instance functions. Look up further in the hierarchy to see if we can deduce the intended context from there
17030
- const ancestorComponent = formElement.parentNode && formElement.parentNode.nodeType === formElement.ELEMENT_NODE && formElement.parentNode.closest('[ref]');
17031
- if (ancestorComponent) {
17032
- const resolver = createNamespaceResolver(ancestorComponent.getAttribute('ref'), ancestorComponent);
17033
- setCachedNamespaceResolver(xpathQuery, formElement, resolver);
17034
- return resolver;
16995
+ /**
16996
+ * Alternative to `contains` that respects shadowroots
16997
+ * @param {Node} ancestor
16998
+ * @param {Node} descendant
16999
+ * @returns {boolean}
17000
+ */
17001
+ static contains(ancestor, descendant) {
17002
+ while (descendant) {
17003
+ if (descendant === ancestor) {
17004
+ return true;
17005
+ }
17006
+ if (descendant.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
17007
+ // We are passing a shadow root boundary
17008
+ descendant = descendant.host;
17009
+ } else {
17010
+ descendant = descendant.parentNode;
17011
+ }
17035
17012
  }
17036
- // Nothing found: let's just assume we're supposed to use the `default` instance
17037
- instanceReferences = ['default'];
17013
+ return false;
17038
17014
  }
17039
- if (instanceReferences.length === 1) {
17040
- // console.log(`resolving ${xpathQuery} with ${instanceReferences[0]}`);
17041
- let instance;
17042
- if (instanceReferences[0] === 'default') {
17043
- /**
17044
- * @type {HTMLElement}
17045
- */
17046
- const actualForeElement = evaluateXPathToFirstNode$1('ancestor-or-self::fx-fore[1]', formElement, null, null, {
17047
- namespaceResolver: xhtmlNamespaceResolver
17048
- });
17049
- instance = actualForeElement && actualForeElement.querySelector('fx-instance');
17050
- } else {
17051
- instance = resolveId(instanceReferences[0], formElement, 'fx-instance');
17052
- }
17053
- if (instance && instance.hasAttribute('xpath-default-namespace')) {
17054
- const xpathDefaultNamespace = instance.getAttribute('xpath-default-namespace');
17055
- /*
17056
- console.log(
17057
- `Resolving the xpath ${xpathQuery} with the default namespace set to ${xpathDefaultNamespace}`,
17058
- );
17059
- */
17060
- /**
17061
- * @type {NamespaceResolver}
17062
- */
17063
- const resolveNamespacePrefix = prefix => {
17064
- if (!prefix) {
17065
- return xpathDefaultNamespace;
17066
- }
17067
- return undefined;
17068
- };
17069
- setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
17070
- return resolveNamespacePrefix;
17015
+
17016
+ /**
17017
+ * Alternative to `closest` that respects subcontrol boundaries
17018
+ *
17019
+ * @param {string} querySelector
17020
+ * @param {Node} start
17021
+ * @returns {HTMLElement}
17022
+ */
17023
+ static getClosest(querySelector, start) {
17024
+ while (start && !start.matches || !start.matches(querySelector)) {
17025
+ if (start.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
17026
+ // We are passing a shadow root boundary
17027
+ start = start.host;
17028
+ continue;
17029
+ }
17030
+ if (start.nodeType === Node.ATTRIBUTE_NODE) {
17031
+ // We are passing an attribute
17032
+ start = start.ownerElement;
17033
+ continue;
17034
+ }
17035
+ if (start.nodeType === Node.TEXT_NODE) {
17036
+ start = start.parentNode;
17037
+ }
17038
+ if (start.matches('fx-fore')) {
17039
+ // Subform reached. Bail out
17040
+ return null;
17041
+ }
17042
+ start = start.parentNode;
17043
+ if (!start) {
17044
+ return null;
17045
+ }
17071
17046
  }
17047
+ return start;
17072
17048
  }
17073
- /*
17074
- if (instanceReferences.length > 1) {
17075
- console.warn(
17076
- `More than one instance is used in the query "${xpathQuery}". The default namespace resolving will be used`,
17077
- );
17078
- }
17079
- */
17080
-
17081
- const xpathDefaultNamespace = evaluateXPathToString$1('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) || '';
17082
17049
 
17083
17050
  /**
17084
- * @type {NamespaceResolver}
17051
+ * returns next bound element upwards in tree
17052
+ * @param {Node} start where to start the search
17053
+ * @returns {*|null}
17085
17054
  */
17086
- const resolveNamespacePrefix = function resolveNamespacePrefix(prefix) {
17087
- if (prefix === '') {
17088
- return xpathDefaultNamespace;
17055
+ static getParentBindingElement(start) {
17056
+ /* if (start.parentNode.host) {
17057
+ const { host } = start.parentNode;
17058
+ if (host.hasAttribute('ref')) {
17059
+ return host;
17060
+ }
17061
+ } else */
17062
+ if (start.parentNode && (start.parentNode.nodeType !== Node.DOCUMENT_NODE || start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)) {
17063
+ return this.getClosest('fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref],fx-repeatitem', start.parentNode);
17089
17064
  }
17065
+ return null;
17066
+ }
17090
17067
 
17091
- // Note: ideally we should use Node#lookupNamespaceURI. However, the nodes we are passed are
17092
- // XML. The best we can do is emulate the `xmlns:xxx` namespace declarations by regarding them as
17093
- // attributes. Which they technically ARE NOT!
17068
+ /**
17069
+ * Checks whether the specified path expression is an absolute path.
17070
+ *
17071
+ * @param {string} path the path expression.
17072
+ * @returns {boolean} <code>true</code> if specified path expression is an absolute
17073
+ * path, otherwise <code>false</code>.
17074
+ */
17075
+ static isAbsolutePath(path) {
17076
+ return path != null && (path.startsWith('/') || path.startsWith('instance(') || path.startsWith('$'));
17077
+ }
17094
17078
 
17095
- return evaluateXPathToString$1('ancestor-or-self::*/@*[name() = "xmlns:" || $prefix][last()]', formElement, null, {
17096
- prefix
17097
- });
17098
- };
17099
- setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
17100
- return resolveNamespacePrefix;
17101
- }
17102
- function createNamespaceResolverForNode(query, contextNode, formElement) {
17103
- if ((contextNode && contextNode.ownerDocument || contextNode) === window.document) {
17104
- // Running a query on the HTML DOM. Don't bother resolving namespaces in any other way
17105
- return xhtmlNamespaceResolver;
17079
+ /**
17080
+ * @param {string} ref
17081
+ */
17082
+ static isSelfReference(ref) {
17083
+ return ref === '.' || ref === './text()' || ref === 'text()' || ref === '' || ref === null;
17106
17084
  }
17107
- return createNamespaceResolver(query, formElement);
17108
- }
17109
17085
 
17110
- /**
17111
- * Implementation of the functionNameResolver passed to FontoXPath to
17112
- * redirect function resolving for unprefixed functions to either the fn or the xf namespace
17113
- */
17114
- // eslint-disable-next-line no-unused-vars
17115
- function functionNameResolver({
17116
- prefix,
17117
- localName
17118
- }, _arity) {
17119
- switch (localName) {
17120
- // TODO: put the full XForms library functions set here
17121
- case 'context':
17122
- case 'base64encode':
17123
- case 'boolean-from-string':
17124
- case 'current':
17125
- case 'depends':
17126
- case 'event':
17127
- case 'fore-attr':
17128
- case 'index':
17129
- case 'instance':
17130
- case 'json2xml':
17131
- case 'xml2Json':
17132
- case 'log':
17133
- case 'parse':
17134
- case 'local-date':
17135
- case 'local-dateTime':
17136
- case 'logtree':
17137
- case 'uri':
17138
- case 'uri-fragment':
17139
- case 'uri-host':
17140
- case 'uri-param':
17141
- case 'uri-path':
17142
- case 'uri-relpath':
17143
- case 'uri-port':
17144
- case 'uri-query':
17145
- case 'uri-scheme':
17146
- case 'uri-scheme-specific-part':
17147
- return {
17148
- namespaceURI: XFORMS_NAMESPACE_URI,
17149
- localName
17150
- };
17151
- default:
17152
- if (prefix === '' && globallyDeclaredFunctionLocalNames.includes(localName)) {
17153
- // The function has been declared without a prefix and is called here without a prefix.
17154
- // Just make this work. It is the developer-friendly way
17155
- return {
17156
- namespaceURI: 'http://www.w3.org/2005/xquery-local-functions',
17157
- localName
17158
- };
17159
- }
17160
- if (prefix === 'fn' || prefix === '') {
17161
- return {
17162
- namespaceURI: 'http://www.w3.org/2005/xpath-functions',
17163
- localName
17164
- };
17086
+ /**
17087
+ * returns the instance id from a complete XPath using `instance()` function.
17088
+ *
17089
+ * Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
17090
+ *
17091
+ * Otherwise instance id is extracted from function and returned. If all fails null is returned.
17092
+ * @param {string} ref
17093
+ * @param {HTMLElement} boundElement The element related to this ref. Used to resolve variables
17094
+ * @returns {string}
17095
+ */
17096
+ static getInstanceId(ref, boundElement) {
17097
+ if (!ref) {
17098
+ return 'default';
17099
+ }
17100
+ if (ref.startsWith('instance()')) {
17101
+ return 'default';
17102
+ }
17103
+ if (ref.startsWith('instance(')) {
17104
+ const result = ref.substring(ref.indexOf('(') + 1);
17105
+ return result.substring(1, result.indexOf(')') - 1);
17106
+ }
17107
+ if (ref.startsWith('$')) {
17108
+ // this variable might actually point to an instance
17109
+ const variableName = ref.match(/\$(?<variableName>[a-zA-Z0-9\-\_]+).*/)?.groups?.variableName;
17110
+ let closestActualFormElement = boundElement;
17111
+ while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
17112
+ closestActualFormElement = closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE ? closestActualFormElement.ownerElement : closestActualFormElement.parentNode;
17165
17113
  }
17166
- if (prefix === 'local') {
17167
- return {
17168
- namespaceURI: 'http://www.w3.org/2005/xquery-local-functions',
17169
- localName
17170
- };
17114
+ const correspondingVariable = closestActualFormElement?.inScopeVariables?.get(variableName);
17115
+ if (!correspondingVariable) {
17116
+ return null;
17171
17117
  }
17172
- return null;
17118
+ return this.getInstanceId(correspondingVariable.valueQuery, correspondingVariable);
17119
+ }
17120
+ return null;
17173
17121
  }
17174
- }
17175
17122
 
17176
- /**
17177
- * Get the variables in scope of the form element. These are the values of the variables that
17178
- * logically precede the formElement that declares the XPath
17179
- *
17180
- * @param {Node} formElement The element that declares the XPath
17181
- *
17182
- * @returns {Object} A key-value mapping of the variables
17183
- */
17184
- function getVariablesInScope(formElement) {
17185
- let closestActualFormElement = formElement;
17186
- while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
17187
- closestActualFormElement = closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE ? closestActualFormElement.ownerElement : closestActualFormElement.parentNode;
17123
+ /**
17124
+ * @param {HTMLElement} boundElement
17125
+ * @param {string} path
17126
+ * @returns {string}
17127
+ */
17128
+ static resolveInstance(boundElement, path) {
17129
+ let instanceId = XPathUtil.getInstanceId(path, boundElement);
17130
+ if (!instanceId) {
17131
+ instanceId = XPathUtil.getInstanceId(boundElement.getAttribute('ref'), boundElement);
17132
+ }
17133
+ if (instanceId !== null) {
17134
+ return instanceId;
17135
+ }
17136
+ const parentBinding = XPathUtil.getParentBindingElement(boundElement);
17137
+ if (parentBinding) {
17138
+ return this.resolveInstance(parentBinding, path);
17139
+ }
17140
+ return 'default';
17188
17141
  }
17189
- if (!closestActualFormElement) {
17190
- return {};
17142
+
17143
+ /**
17144
+ * @param {Node} node
17145
+ * @returns string
17146
+ */
17147
+ /*
17148
+ static getDocPath(node) {
17149
+ const path = fx.evaluateXPathToString('path()', node);
17150
+ // Path is like `$default/x[1]/y[1]`
17151
+ const shortened = XPathUtil.shortenPath(path);
17152
+ return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
17191
17153
  }
17192
- const variables = {};
17193
- if (closestActualFormElement.inScopeVariables) {
17194
- for (const key of closestActualFormElement.inScopeVariables.keys()) {
17195
- const varElementOrValue = closestActualFormElement.inScopeVariables.get(key);
17196
- if (!varElementOrValue) {
17197
- continue;
17198
- }
17199
- if (varElementOrValue.nodeType) {
17200
- // We are a var element, set the value to the value computed there
17201
- variables[key] = varElementOrValue.value;
17202
- // variables[key] = varElementOrValue.inScopeVariables.get(key);
17203
- } else {
17204
- // We are a direct value. This is used to leak in event variables
17205
- variables[key] = varElementOrValue;
17206
- }
17207
- }
17154
+ */
17155
+
17156
+ /**
17157
+ * @param {Node} node
17158
+ * @param {string} instanceId
17159
+ * @returns string
17160
+ */
17161
+ static getPath(node, instanceId) {
17162
+ const path = evaluateXPathToString$1('path()', node);
17163
+ // Path is like `$default/x[1]/y[1]`
17164
+ const shortened = XPathUtil.shortenPath(path);
17165
+ return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
17166
+ }
17167
+
17168
+ /**
17169
+ * @param {string} path
17170
+ * @returns string
17171
+ */
17172
+ static shortenPath(path) {
17173
+ const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
17174
+ if (tmp === 'root()') return tmp;
17175
+ // cut off leading slash
17176
+ const tmp1 = tmp.substring(1, tmp.length);
17177
+ // ### cut-off root node ref
17178
+ return tmp1.substring(tmp1.indexOf('/'), tmp.length);
17179
+ }
17180
+
17181
+ /**
17182
+ * @param {string} dep
17183
+ * @returns {string}
17184
+ */
17185
+ static getBasePath(dep) {
17186
+ const split = dep.split(':');
17187
+ return split[0];
17208
17188
  }
17209
- return variables;
17210
17189
  }
17211
17190
 
17212
17191
  /**
17213
- * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
17214
- * type of the output is more predictable.
17192
+ * A simple dependency graph
17193
+ *
17194
+ * based on the work of https://github.com/jriecken/dependency-graph but working on ES6.
17195
+ *
17196
+ * Furthermore instead of the DepGraphCycleError a compute-exception event is dispatched.
17215
17197
  *
17216
- * @param {string} xpath The XPath to run
17217
- * @param {Node} contextNode The start of the XPath
17218
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
17219
- * @param {Object} variables Any variables to pass to the XPath
17220
- * @param {Object} options Any options to pass to the XPath
17221
17198
  *
17222
- * @returns {any[]}
17223
17199
  */
17224
- /*
17225
- export function evaluateXPath(xpath, contextNode, formElement, variables = {}, options={}, domFacade = null) {
17226
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17227
- const variablesInScope = getVariablesInScope(formElement);
17228
-
17229
- return fxEvaluateXPath(
17230
- xpath,
17231
- contextNode,
17232
- domFacade,
17233
- {...variablesInScope, ...variables},
17234
- fxEvaluateXPath.ALL_RESULTS_TYPE,
17235
- {
17236
- debug: true,
17237
- currentContext: {formElement, variables},
17238
- moduleImports: {
17239
- xf: XFORMS_NAMESPACE_URI,
17240
- },
17241
- functionNameResolver,
17242
- namespaceResolver,
17243
- language: options.language || evaluateXPath.XPATH_3_1
17244
- },
17245
- );
17246
- }
17247
- */
17248
- function evaluateXPath(xpath, contextNode, formElement, variables = {}, options = {}) {
17249
- try {
17250
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17251
- const variablesInScope = getVariablesInScope(formElement);
17252
- const result = evaluateXPath$1(xpath, contextNode, null, {
17253
- ...variablesInScope,
17254
- ...variables
17255
- }, evaluateXPath$1.ALL_RESULTS_TYPE, {
17256
- xmlSerializer: new XMLSerializer(),
17257
- debug: true,
17258
- currentContext: {
17259
- formElement,
17260
- variables
17261
- },
17262
- moduleImports: {
17263
- xf: XFORMS_NAMESPACE_URI
17264
- },
17265
- functionNameResolver,
17266
- namespaceResolver,
17267
- language: options.language || evaluateXPath$1.XPATH_3_1_LANGUAGE
17268
- });
17269
- // console.log('evaluateXPath',xpath, result);
17270
- return result;
17271
- } catch (e) {
17272
- formElement.dispatchEvent(new CustomEvent('error', {
17273
- composed: false,
17274
- bubbles: true,
17275
- detail: {
17276
- origin: formElement,
17277
- message: `Expression '${xpath}' failed: ${e}`,
17278
- expr: xpath,
17279
- level: 'Error'
17280
- }
17281
- }));
17282
17200
 
17283
- /*
17284
- formElement.dispatchEvent(
17285
- new CustomEvent('error', {
17286
- composed: false,
17287
- bubbles: true,
17288
- cancelable:true,
17289
- detail: {
17290
- origin: formElement,
17291
- message: `Expression '${xpath}' failed`,
17292
- expr:xpath,
17293
- level:'Error'},
17294
- }),
17295
- );
17296
- */
17297
- // Return 'nothing' in hope the rest of the page can forgive this
17298
- return [];
17299
- }
17300
- }
17301
17201
  /**
17302
- * Evaluate an XPath to the first Node
17303
- *
17304
- * @param {string} xpath The XPath to run
17305
- * @param {Node} contextNode The start of the XPath
17306
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
17307
- * @returns {Node} The first node found in the XPath
17202
+ * Cycle error, including the path of the cycle.
17308
17203
  */
17309
- function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
17310
- try {
17311
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17312
- const variablesInScope = getVariablesInScope(formElement);
17313
- const result = evaluateXPathToFirstNode$1(xpath, contextNode, null, variablesInScope, {
17314
- defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
17315
- moduleImports: {
17316
- xf: XFORMS_NAMESPACE_URI
17317
- },
17318
- currentContext: {
17319
- formElement
17320
- },
17321
- functionNameResolver,
17322
- namespaceResolver,
17323
- xmlSerializer: new XMLSerializer()
17324
- });
17325
- // console.log('evaluateXPathToFirstNode',xpath, result);
17326
- return result;
17327
- } catch (e) {
17328
- formElement.dispatchEvent(new CustomEvent('error', {
17329
- composed: false,
17330
- bubbles: true,
17331
- detail: {
17332
- origin: formElement,
17333
- message: `Expression '${xpath}' failed: ${e}`,
17334
- expr: xpath,
17335
- level: 'Error'
17336
- }
17337
- }));
17204
+ // const DepGraphCycleError = (exports.DepGraphCycleError = function (cyclePath) {
17205
+
17206
+ /*
17207
+ export function DepGraphCycleError(cyclePath) {
17208
+ const message = "Dependency Cycle Found: " + cyclePath.join(" -> ");
17209
+ const instance = new Error(message);
17210
+ instance.cyclePath = cyclePath;
17211
+ Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
17212
+ if (Error.captureStackTrace) {
17213
+ Error.captureStackTrace(instance, DepGraphCycleError);
17338
17214
  }
17339
- return null;
17340
- }
17215
+ return instance;
17216
+ };
17341
17217
 
17342
- /**
17343
- * Evaluate an XPath to all nodes
17344
- *
17345
- * @param {string} xpath The XPath to run
17346
- * @param {Node} contextNode The start of the XPath
17347
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
17348
- * @return {Node[]} All nodes
17349
- */
17350
- function evaluateXPathToNodes(xpath, contextNode, formElement) {
17351
- try {
17352
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17353
- const variablesInScope = getVariablesInScope(formElement);
17354
- const result = evaluateXPathToNodes$1(xpath, contextNode, null, variablesInScope, {
17355
- currentContext: {
17356
- formElement
17357
- },
17358
- functionNameResolver,
17359
- moduleImports: {
17360
- xf: XFORMS_NAMESPACE_URI
17361
- },
17362
- namespaceResolver,
17363
- xmlSerializer: new XMLSerializer()
17364
- });
17365
- // console.log('evaluateXPathToNodes',xpath, result);
17366
- return result;
17367
- } catch (e) {
17368
- formElement.dispatchEvent(new CustomEvent('error', {
17369
- composed: false,
17370
- bubbles: true,
17371
- detail: {
17372
- origin: formElement,
17373
- message: `Expression '${xpath}' failed: ${e}`,
17374
- expr: xpath,
17375
- level: 'Error'
17376
- }
17377
- }));
17218
+ DepGraphCycleError.prototype = Object.create(Error.prototype, {
17219
+ constructor: {
17220
+ value: Error,
17221
+ enumerable: false,
17222
+ writable: true,
17223
+ configurable: true
17378
17224
  }
17379
- }
17225
+ });
17226
+ Object.setPrototypeOf(DepGraphCycleError, Error);
17227
+ */
17380
17228
 
17381
17229
  /**
17382
- * Evaluate an XPath to a boolean
17230
+ * Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
17383
17231
  *
17384
- * @param {string} xpath The XPath to run
17385
- * @param {Node} contextNode The start of the XPath
17386
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
17387
- * @return {boolean}
17232
+ * Detects cycles and throws an Error if one is detected (unless the "circular"
17233
+ * parameter is "true" in which case it ignores them).
17234
+ *
17235
+ * @param edges The set of edges to DFS through
17236
+ * @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
17237
+ * @param result An array in which the results will be populated
17238
+ * @param circular A boolean to allow circular dependencies
17388
17239
  */
17389
- function evaluateXPathToBoolean(xpath, contextNode, formElement) {
17390
- try {
17391
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17392
- const variablesInScope = getVariablesInScope(formElement);
17393
- return evaluateXPathToBoolean$1(xpath, contextNode, null, variablesInScope, {
17394
- currentContext: {
17395
- formElement
17396
- },
17397
- functionNameResolver,
17398
- moduleImports: {
17399
- xf: XFORMS_NAMESPACE_URI
17400
- },
17401
- namespaceResolver,
17402
- xmlSerializer: new XMLSerializer()
17240
+ function createDFS(edges, leavesOnly, result, circular) {
17241
+ const visited = {};
17242
+ // eslint-disable-next-line func-names
17243
+ return function (start) {
17244
+ // console.log('start ', start);
17245
+ if (visited[start]) {
17246
+ return;
17247
+ }
17248
+ const inCurrentPath = {};
17249
+ const currentPath = [];
17250
+ const todo = []; // used as a stack
17251
+ todo.push({
17252
+ node: start,
17253
+ processed: false
17403
17254
  });
17404
- } catch (e) {
17405
- formElement.dispatchEvent(new CustomEvent('error', {
17406
- composed: false,
17407
- bubbles: true,
17408
- detail: {
17409
- origin: formElement,
17410
- message: `Expression '${xpath}' failed: ${e}`,
17411
- expr: xpath,
17412
- level: 'Error'
17255
+ while (todo.length > 0) {
17256
+ const current = todo[todo.length - 1]; // peek at the todo stack
17257
+ const {
17258
+ processed
17259
+ } = current;
17260
+ const {
17261
+ node
17262
+ } = current;
17263
+ if (!processed) {
17264
+ // Haven't visited edges yet (visiting phase)
17265
+ if (visited[node]) {
17266
+ todo.pop();
17267
+ // eslint-disable-next-line no-continue
17268
+ continue;
17269
+ } else if (inCurrentPath[node]) {
17270
+ // It's not a DAG
17271
+ if (circular) {
17272
+ todo.pop();
17273
+ // If we're tolerating cycles, don't revisit the node
17274
+ // eslint-disable-next-line no-continue
17275
+ continue;
17276
+ }
17277
+ currentPath.push(node);
17278
+ window.dispatchEvent(new CustomEvent('compute-exception', {
17279
+ composed: false,
17280
+ bubbles: true,
17281
+ detail: {
17282
+ path: currentPath,
17283
+ message: 'cyclic graph'
17284
+ }
17285
+ }));
17286
+ // return;
17287
+ // console.log('‘circular path: ' + currentPath);
17288
+ // throw new DepGraphCycleError(currentPath);
17289
+
17290
+ // Stop all processing. This form is broken and we should not break the browser
17291
+ throw new Error(`Cyclic at ${currentPath}`);
17292
+ }
17293
+ inCurrentPath[node] = true;
17294
+ currentPath.push(node);
17295
+ const nodeEdges = edges[node];
17296
+ // (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation)
17297
+ for (let i = nodeEdges.length - 1; i >= 0; i -= 1) {
17298
+ todo.push({
17299
+ node: nodeEdges[i],
17300
+ processed: false
17301
+ });
17302
+ }
17303
+ current.processed = true;
17304
+ } else {
17305
+ // Have visited edges (stack unrolling phase)
17306
+ todo.pop();
17307
+ currentPath.pop();
17308
+ inCurrentPath[node] = false;
17309
+ visited[node] = true;
17310
+ if (!leavesOnly || edges[node].length === 0) {
17311
+ result.push(node);
17312
+ }
17413
17313
  }
17414
- }));
17415
- }
17314
+ }
17315
+ };
17416
17316
  }
17417
17317
 
17418
17318
  /**
17419
- * Evaluate an XPath to a string
17420
- *
17421
- * @param {string} xpath The XPath to run
17422
- * @param {Node} contextNode The start of the XPath
17423
- * @param {Node} formElement The form element associated to the XPath
17424
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
17425
- * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
17426
- * access. This is used to determine dependencies between bind elements.
17427
- * @return {string}
17319
+ * Simple Dependency Graph
17428
17320
  */
17429
- function evaluateXPathToString(xpath, contextNode, formElement, domFacade = null) {
17430
- try {
17431
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17432
- const variablesInScope = getVariablesInScope(formElement);
17433
- return evaluateXPathToString$1(xpath, contextNode, domFacade, variablesInScope, {
17434
- currentContext: {
17435
- formElement
17436
- },
17437
- functionNameResolver,
17438
- moduleImports: {
17439
- xf: XFORMS_NAMESPACE_URI
17440
- },
17441
- namespaceResolver,
17442
- xmlSerializer: new XMLSerializer()
17443
- });
17444
- } catch (e) {
17445
- formElement.dispatchEvent(new CustomEvent('error', {
17446
- composed: false,
17447
- bubbles: true,
17448
- detail: {
17449
- origin: formElement,
17450
- message: `Expression '${xpath}' failed: ${e}`,
17451
- expr: xpath,
17452
- level: 'Error'
17453
- }
17454
- }));
17455
- }
17321
+
17322
+ /*
17323
+ var DepGraph = (exports.DepGraph = function DepGraph(opts) {
17324
+ this.nodes = {}; // Node -> Node/Data (treated like a Set)
17325
+ this.outgoingEdges = {}; // Node -> [Dependency Node]
17326
+ this.incomingEdges = {}; // Node -> [Dependant Node]
17327
+ this.circular = opts && !!opts.circular; // Allows circular deps
17328
+ });
17329
+ */
17330
+
17331
+ function DepGraph(opts) {
17332
+ this.nodes = {}; // Node -> Node/Data (treated like a Set)
17333
+ this.outgoingEdges = {}; // Node -> [Dependency Node]
17334
+ this.incomingEdges = {}; // Node -> [Dependant Node]
17335
+ this.circular = opts && !!opts.circular; // Allows circular deps
17456
17336
  }
17457
17337
 
17458
- /**
17459
- * Evaluate an XPath to a set of strings
17460
- *
17461
- * @param {string} xpath The XPath to run
17462
- * @param {Node} contextNode The start of the XPath
17463
- * @param {Node} formElement The form element associated to the XPath
17464
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
17465
- * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
17466
- * access. This is used to determine dependencies between bind elements.
17467
- * @return {string[]}
17468
- */
17469
- function evaluateXPathToStrings(xpath, contextNode, formElement, domFacade = null) {
17470
- try {
17471
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17472
- return evaluateXPathToStrings$1(xpath, contextNode, domFacade, {}, {
17473
- currentContext: {
17474
- formElement
17475
- },
17476
- functionNameResolver,
17477
- moduleImports: {
17478
- xf: XFORMS_NAMESPACE_URI
17479
- },
17480
- namespaceResolver,
17481
- xmlSerializer: new XMLSerializer()
17482
- });
17483
- } catch (e) {
17484
- formElement.dispatchEvent(new CustomEvent('error', {
17485
- composed: false,
17486
- bubbles: true,
17487
- detail: {
17488
- origin: formElement,
17489
- message: `Expression '${xpath}' failed: ${e}`,
17490
- expr: xpath,
17491
- level: 'Error'
17338
+ DepGraph.prototype = {
17339
+ /**
17340
+ * The number of nodes in the graph.
17341
+ */
17342
+ size() {
17343
+ return Object.keys(this.nodes).length;
17344
+ },
17345
+ /**
17346
+ * Add a node to the dependency graph. If a node already exists, this method will do nothing.
17347
+ */
17348
+ addNode(node, data) {
17349
+ if (!this.hasNode(node)) {
17350
+ // Checking the arguments length allows the user to add a node with undefined data
17351
+ if (arguments.length === 2) {
17352
+ this.nodes[node] = data;
17353
+ } else {
17354
+ this.nodes[node] = node;
17492
17355
  }
17493
- }));
17494
- }
17495
- }
17356
+ this.outgoingEdges[node] = [];
17357
+ this.incomingEdges[node] = [];
17358
+ }
17359
+ },
17360
+ /**
17361
+ * Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
17362
+ */
17363
+ removeNode(node) {
17364
+ if (this.hasNode(node)) {
17365
+ delete this.nodes[node];
17366
+ delete this.outgoingEdges[node];
17367
+ delete this.incomingEdges[node];
17368
+ // [this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) {
17369
+ [this.incomingEdges, this.outgoingEdges].forEach(edgeList => {
17370
+ Object.keys(edgeList).forEach(key => {
17371
+ const idx = edgeList[key].indexOf(node);
17372
+ if (idx >= 0) {
17373
+ edgeList[key].splice(idx, 1);
17374
+ }
17375
+ }, this);
17376
+ });
17377
+ }
17378
+ },
17379
+ /**
17380
+ * Check if a node exists in the graph
17381
+ */
17382
+ hasNode(node) {
17383
+ // return this.nodes.hasOwnProperty(node);
17496
17384
 
17497
- /**
17498
- * Evaluate an XPath to a number
17499
- *
17500
- * @param {string} xpath The XPath to run
17501
- * @param {Node} contextNode The start of the XPath
17502
- * @param {Node} formElement The form element associated to the XPath
17503
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
17504
- * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
17505
- * access. This is used to determine dependencies between bind elements.
17506
- * @return {number}
17507
- */
17508
- function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade = null) {
17509
- try {
17510
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17511
- const variablesInScope = getVariablesInScope(formElement);
17512
- return evaluateXPathToNumber$1(xpath, contextNode, domFacade, variablesInScope, {
17513
- currentContext: {
17514
- formElement
17515
- },
17516
- functionNameResolver,
17517
- moduleImports: {
17518
- xf: XFORMS_NAMESPACE_URI
17519
- },
17520
- namespaceResolver,
17521
- xmlSerializer: new XMLSerializer()
17385
+ return Object.prototype.hasOwnProperty.call(this.nodes, node);
17386
+ },
17387
+ /**
17388
+ * Get the data associated with a node name
17389
+ */
17390
+ getNodeData(node) {
17391
+ if (this.hasNode(node)) {
17392
+ return this.nodes[node];
17393
+ }
17394
+ throw new Error(`Node does not exist: ${node}`);
17395
+ },
17396
+ /**
17397
+ * Set the associated data for a given node name. If the node does not exist, this method will throw an error
17398
+ */
17399
+ setNodeData(node, data) {
17400
+ if (this.hasNode(node)) {
17401
+ this.nodes[node] = data;
17402
+ } else {
17403
+ throw new Error(`Node does not exist: ${node}`);
17404
+ }
17405
+ },
17406
+ /**
17407
+ * Add a dependency between two nodes. If either of the nodes does not exist,
17408
+ * an Error will be thrown.
17409
+ */
17410
+ addDependency(from, to) {
17411
+ if (!this.hasNode(from)) {
17412
+ throw new Error(`Node does not exist: ${from}`);
17413
+ }
17414
+ if (!this.hasNode(to)) {
17415
+ throw new Error(`Node does not exist: ${to}`);
17416
+ }
17417
+ if (this.outgoingEdges[from].indexOf(to) === -1) {
17418
+ this.outgoingEdges[from].push(to);
17419
+ }
17420
+ if (this.incomingEdges[to].indexOf(from) === -1) {
17421
+ this.incomingEdges[to].push(from);
17422
+ }
17423
+ return true;
17424
+ },
17425
+ /**
17426
+ * Remove a dependency between two nodes.
17427
+ */
17428
+ removeDependency(from, to) {
17429
+ let idx;
17430
+ if (this.hasNode(from)) {
17431
+ idx = this.outgoingEdges[from].indexOf(to);
17432
+ if (idx >= 0) {
17433
+ this.outgoingEdges[from].splice(idx, 1);
17434
+ }
17435
+ }
17436
+ if (this.hasNode(to)) {
17437
+ idx = this.incomingEdges[to].indexOf(from);
17438
+ if (idx >= 0) {
17439
+ this.incomingEdges[to].splice(idx, 1);
17440
+ }
17441
+ }
17442
+ },
17443
+ /**
17444
+ * Return a clone of the dependency graph. If any custom data is attached
17445
+ * to the nodes, it will only be shallow copied.
17446
+ */
17447
+ clone() {
17448
+ const source = this;
17449
+ const result = new DepGraph();
17450
+ const keys = Object.keys(source.nodes);
17451
+ keys.forEach(n => {
17452
+ result.nodes[n] = source.nodes[n];
17453
+ result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
17454
+ result.incomingEdges[n] = source.incomingEdges[n].slice(0);
17522
17455
  });
17523
- } catch (e) {
17524
- formElement.dispatchEvent(new CustomEvent('error', {
17525
- composed: false,
17526
- bubbles: true,
17527
- detail: {
17528
- origin: formElement,
17529
- message: `Expression '${xpath}' failed: ${e}`,
17530
- expr: xpath,
17531
- level: 'Error'
17456
+ return result;
17457
+ },
17458
+ /**
17459
+ * Get an array containing the direct dependencies of the specified node.
17460
+ *
17461
+ * Throws an Error if the specified node does not exist.
17462
+ */
17463
+ directDependenciesOf(node) {
17464
+ if (this.hasNode(node)) {
17465
+ return this.outgoingEdges[node].slice(0);
17466
+ }
17467
+ throw new Error(`Node does not exist: ${node}`);
17468
+ },
17469
+ /**
17470
+ * Get an array containing the nodes that directly depend on the specified node.
17471
+ *
17472
+ * Throws an Error if the specified node does not exist.
17473
+ */
17474
+ directDependantsOf(node) {
17475
+ if (this.hasNode(node)) {
17476
+ return this.incomingEdges[node].slice(0);
17477
+ }
17478
+ throw new Error(`Node does not exist: ${node}`);
17479
+ },
17480
+ /**
17481
+ * Get an array containing the nodes that the specified node depends on (transitively).
17482
+ *
17483
+ * Throws an Error if the graph has a cycle, or the specified node does not exist.
17484
+ *
17485
+ * If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
17486
+ * in the array.
17487
+ */
17488
+ dependenciesOf(node, leavesOnly) {
17489
+ if (this.hasNode(node)) {
17490
+ const result = [];
17491
+ const DFS = createDFS(this.outgoingEdges, leavesOnly, result, this.circular);
17492
+ DFS(node);
17493
+ const idx = result.indexOf(node);
17494
+ if (idx >= 0) {
17495
+ result.splice(idx, 1);
17532
17496
  }
17533
- }));
17534
- }
17535
- }
17536
- const contextFunction = (dynamicContext, string) => {
17537
- const caller = dynamicContext.currentContext.formElement;
17538
- let instance = null;
17539
- if (string) {
17540
- instance = resolveId(string, caller);
17541
- } else {
17542
- instance = XPathUtil.getParentBindingElement(caller);
17543
- }
17544
- if (instance) {
17545
- if (instance.nodeName === 'FX-REPEAT') {
17546
- const {
17547
- nodeset
17548
- } = instance;
17549
- for (let parent = caller; parent; parent = parent.parentNode) {
17550
- if (parent.parentNode === instance) {
17551
- const offset = Array.from(parent.parentNode.children).indexOf(parent);
17552
- return nodeset[offset];
17553
- }
17497
+ return result;
17498
+ }
17499
+ throw new Error(`Node does not exist: ${node}`);
17500
+ },
17501
+ /**
17502
+ * get an array containing the nodes that depend on the specified node (transitively).
17503
+ *
17504
+ * Throws an Error if the graph has a cycle, or the specified node does not exist.
17505
+ *
17506
+ * If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
17507
+ */
17508
+ dependantsOf(node, leavesOnly) {
17509
+ if (this.hasNode(node)) {
17510
+ const result = [];
17511
+ const DFS = createDFS(this.incomingEdges, leavesOnly, result, this.circular);
17512
+ DFS(node);
17513
+ const idx = result.indexOf(node);
17514
+ if (idx >= 0) {
17515
+ result.splice(idx, 1);
17554
17516
  }
17517
+ return result;
17518
+ }
17519
+ throw new Error(`Node does not exist: ${node}`);
17520
+ },
17521
+ /**
17522
+ * Get an array of nodes that have no dependants (i.e. nothing depends on them).
17523
+ */
17524
+ entryNodes() {
17525
+ const self = this;
17526
+ return Object.keys(this.nodes).filter(node => self.incomingEdges[node].length === 0);
17527
+ },
17528
+ /**
17529
+ * Construct the overall processing order for the dependency graph.
17530
+ *
17531
+ * Throws an Error if the graph has a cycle.
17532
+ *
17533
+ * If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
17534
+ */
17535
+ overallOrder(leavesOnly) {
17536
+ const self = this;
17537
+ const result = [];
17538
+ const keys = Object.keys(this.nodes);
17539
+ if (keys.length === 0) {
17540
+ return result; // Empty graph
17555
17541
  }
17556
- return instance.nodeset;
17557
- }
17558
- return caller.getInScopeContext();
17559
- };
17560
-
17561
- // todo: implement
17562
- const currentFunction = (dynamicContext, string) => {
17563
- dynamicContext.currentContext.formElement;
17564
- return null;
17565
- };
17566
- const elementFunction = (dynamicContext, string) => {
17567
- dynamicContext.currentContext.formElement;
17568
- const newElement = document.createElement(string);
17569
- return newElement;
17570
- };
17571
-
17572
- /**
17573
- * @param id as string
17574
- * @return instance data for given id serialized to string.
17575
- */
17576
- registerCustomXPathFunction({
17577
- namespaceURI: XFORMS_NAMESPACE_URI,
17578
- localName: 'context'
17579
- }, [], 'item()?', contextFunction);
17580
17542
 
17581
- /**
17582
- * @param id as string
17583
- * @return instance data for given id serialized to string.
17584
- */
17585
- registerCustomXPathFunction({
17586
- namespaceURI: XFORMS_NAMESPACE_URI,
17587
- localName: 'context'
17588
- }, ['xs:string'], 'item()?', contextFunction);
17589
- registerCustomXPathFunction({
17590
- namespaceURI: XFORMS_NAMESPACE_URI,
17591
- localName: 'current'
17592
- }, ['xs:string'], 'item()?', currentFunction);
17593
- registerCustomXPathFunction({
17594
- namespaceURI: XFORMS_NAMESPACE_URI,
17595
- localName: 'element'
17596
- }, ['xs:string'], 'item()?', elementFunction);
17543
+ if (!this.circular) {
17544
+ // Look for cycles - we run the DFS starting at all the nodes in case there
17545
+ // are several disconnected subgraphs inside this dependency graph.
17546
+ const CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
17547
+ keys.forEach(n => {
17548
+ CycleDFS(n);
17549
+ });
17550
+ }
17551
+ const DFS = createDFS(this.outgoingEdges, leavesOnly, result, this.circular);
17552
+ // Find all potential starting points (nodes with nothing depending on them) an
17553
+ // run a DFS starting at these points to get the order
17554
+ keys.filter(node => self.incomingEdges[node].length === 0).forEach(n => {
17555
+ DFS(n);
17556
+ });
17597
17557
 
17598
- /**
17599
- * @param id as string
17600
- * @return instance data for given id serialized to string.
17601
- */
17602
- registerCustomXPathFunction({
17603
- namespaceURI: XFORMS_NAMESPACE_URI,
17604
- localName: 'log'
17605
- }, ['xs:string?'], 'xs:string?', (dynamicContext, string) => {
17606
- const {
17607
- formElement
17608
- } = dynamicContext.currentContext;
17609
- const instance = resolveId(string, formElement, 'fx-instance');
17610
- if (instance) {
17611
- if (instance.getAttribute('type') === 'json') {
17612
- console.warn('log() does not work for JSON yet');
17613
- // return JSON.stringify(instance.getDefaultContext());
17614
- } else {
17615
- const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
17616
- return prettifyXml(def);
17558
+ // If we're allowing cycles - we need to run the DFS against any remaining
17559
+ // nodes that did not end up in the initial result (as they are part of a
17560
+ // subgraph that does not have a clear starting point)
17561
+ if (this.circular) {
17562
+ keys.filter(node => result.indexOf(node) === -1).forEach(n => DFS(n));
17617
17563
  }
17564
+ return result;
17618
17565
  }
17619
- return null;
17620
- });
17621
- registerCustomXPathFunction({
17622
- namespaceURI: XFORMS_NAMESPACE_URI,
17623
- localName: 'fore-attr'
17624
- }, ['xs:string?'], 'xs:string?', (dynamicContext, string) => {
17625
- const {
17626
- formElement
17627
- } = dynamicContext.currentContext;
17628
- let parent = formElement;
17629
- if (formElement.nodeType === Node.TEXT_NODE) {
17630
- parent = formElement.parentNode;
17631
- }
17632
- const foreElement = parent.closest('fx-fore');
17633
- if (foreElement.hasAttribute(string)) {
17634
- return foreElement.getAttribute(string);
17635
- }
17636
- return null;
17637
- });
17638
- registerCustomXPathFunction({
17639
- namespaceURI: XFORMS_NAMESPACE_URI,
17640
- localName: 'parse'
17641
- }, ['xs:string?'], 'element()?', (_dynamicContext, string) => {
17642
- const parser = new DOMParser();
17643
- const out = parser.parseFromString(string, 'application/xml');
17644
- console.log('parse', out);
17566
+ };
17645
17567
 
17646
- /*
17647
- const {formElement} = dynamicContext.currentContext;
17648
- const instance = resolveId(string, formElement, 'fx-instance');
17649
- if (instance) {
17650
- if (instance.getAttribute('type') === 'json') {
17651
- console.warn('log() does not work for JSON yet');
17652
- // return JSON.stringify(instance.getDefaultContext());
17653
- } else {
17654
- const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
17655
- return Fore.prettifyXml(def);
17656
- }
17657
- }
17658
- */
17659
- return out.firstElementChild;
17660
- });
17661
- function buildTree(tree, data) {
17662
- if (!data) return;
17663
- if (data.nodeType === Node.ELEMENT_NODE) {
17664
- if (data.children) {
17665
- const details = document.createElement('details');
17666
- details.setAttribute('data-path', data.nodeName);
17667
- const summary = document.createElement('summary');
17668
- let display = ` <${data.nodeName}`;
17669
- Array.from(data.attributes).forEach(attr => {
17670
- display += ` ${attr.nodeName}="${attr.nodeValue}"`;
17671
- });
17672
- let contents;
17673
- if (data.firstChild && data.firstChild.nodeType === Node.TEXT_NODE && data.firstChild.data.trim() !== '') {
17674
- // console.log('whoooooooooopp');
17675
- contents = data.firstChild.nodeValue;
17676
- display += `>${contents}</${data.nodeName}>`;
17677
- } else {
17678
- display += '>';
17679
- }
17680
- summary.textContent = display;
17681
- details.appendChild(summary);
17682
- if (data.childElementCount !== 0) {
17683
- details.setAttribute('open', 'open');
17684
- } else {
17685
- summary.setAttribute('style', 'list-style:none;');
17686
- }
17687
- tree.appendChild(details);
17688
- Array.from(data.children).forEach(child => {
17689
- // if(child.nodeType === Node.ELEMENT_NODE){
17690
- // child.parentNode.appendChild(buildTree(child));
17691
- buildTree(details, child);
17692
- // }
17693
- });
17694
- }
17695
- } /* else if(data.nodeType === Node.ATTRIBUTE_NODE){
17696
- //create span for now
17697
- // const span = document.createElement('span');
17698
- // span.style.background = 'grey';
17699
- // span.textContent = data.value;
17700
- // tree.appendChild(span);
17701
- tree.setAttribute(data.nodeName,data.value);
17702
- }else {
17703
- tree.textContent = data;
17704
- } */
17568
+ // Create some aliases
17569
+ DepGraph.prototype.directDependentsOf = DepGraph.prototype.directDependantsOf;
17570
+ DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
17705
17571
 
17706
- // return tree;
17572
+ function prettifyXml(source) {
17573
+ const xmlDoc = new DOMParser().parseFromString(source, 'application/xml');
17574
+ const xsltDoc = new DOMParser().parseFromString([
17575
+ // describes how we want to modify the XML - indent everything
17576
+ '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">', ' <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>', ' <xsl:strip-space elements="*"/>', ' <xsl:template match="text()">',
17577
+ // change to just text() to strip space in text nodes
17578
+ ' <xsl:value-of select="normalize-space(.)"/>', ' </xsl:template>', ' <xsl:template match="node()|@*">', ' <xsl:copy>', ' <xsl:apply-templates select="node()|@*"/>', ' </xsl:copy>', ' </xsl:template>', '</xsl:stylesheet>'].join('\n'), 'application/xml');
17579
+ const xsltProcessor = new XSLTProcessor();
17580
+ xsltProcessor.importStylesheet(xsltDoc);
17581
+ const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
17582
+ const resultXml = new XMLSerializer().serializeToString(resultDoc);
17583
+ return resultXml;
17707
17584
  }
17708
17585
 
17709
- registerCustomXPathFunction({
17710
- namespaceURI: XFORMS_NAMESPACE_URI,
17711
- localName: 'logtree'
17712
- }, ['xs:string?'], 'element()?', (dynamicContext, string) => {
17713
- const {
17714
- formElement
17715
- } = dynamicContext.currentContext;
17716
- const instance = resolveId(string, formElement, 'fx-instance');
17717
- if (instance) {
17718
- // const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
17719
- // const def = JSON.stringify(instance.getDefaultContext());
17586
+ const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
17587
+ const createdNamespaceResolversByXPathQueryAndNode = new Map();
17720
17588
 
17721
- const treeDiv = document.createElement('div');
17722
- treeDiv.setAttribute('class', 'logtree');
17723
- // const datatree = buildTree(tree,instance.getDefaultContext());
17724
- // return tree.appendChild(datatree);
17725
- // return buildTree(root,instance.getDefaultContext());;
17726
- const form = dynamicContext.currentContext.formElement;
17727
- const logtree = form.querySelector('.logtree');
17728
- if (logtree) {
17729
- logtree.parentNode.removeChild(logtree);
17730
- }
17731
- const tree = buildTree(treeDiv, instance.getDefaultContext());
17732
- if (tree) {
17733
- form.appendChild(tree);
17734
- }
17589
+ // A global registry of function names that are declared in Fore by a developer using the
17590
+ // `fx-function` element. These should be available without providing a prefix as well
17591
+ const globallyDeclaredFunctionLocalNames = [];
17592
+ function getCachedNamespaceResolver(xpath, node) {
17593
+ if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
17594
+ return null;
17735
17595
  }
17736
- return null;
17737
- });
17738
- const instance = (dynamicContext, string) => {
17739
- // Spec: https://www.w3.org/TR/xforms-xpath/#The_XForms_Function_Library#The_instance.28.29_Function
17740
- // TODO: handle no string passed (null will be passed instead)
17741
-
17742
- /**
17743
- * @type {import('./fx-fore.js').FxFore}
17744
- */
17745
- const formElement = evaluateXPathToFirstNode$1('ancestor-or-self::fx-fore[1]', dynamicContext.currentContext.formElement, null, null, {
17746
- namespaceResolver: xhtmlNamespaceResolver
17747
- });
17748
- let lookup = null;
17749
- if (string === null || string === 'default') {
17750
- lookup = formElement.getModel().getDefaultInstance();
17751
- } else {
17752
- lookup = formElement.getModel().getInstance(string);
17753
- if (!lookup) {
17754
- document.querySelector('fx-fore').dispatchEvent(new CustomEvent('error', {
17755
- composed: true,
17756
- bubbles: true,
17757
- detail: {
17758
- origin: 'functions',
17759
- message: `Instance not found '${string}'`,
17760
- level: 'Error'
17761
- }
17762
- }));
17763
- }
17596
+ return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
17597
+ }
17598
+ function setCachedNamespaceResolver(xpath, node, resolver) {
17599
+ if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
17600
+ return createdNamespaceResolversByXPathQueryAndNode.set(xpath, new Map());
17764
17601
  }
17765
- const context = lookup.getDefaultContext();
17766
- if (!context) {
17767
- return null;
17602
+ return createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
17603
+ }
17604
+ const xhtmlNamespaceResolver = prefix => {
17605
+ if (!prefix) {
17606
+ return 'http://www.w3.org/1999/xhtml';
17768
17607
  }
17769
- return context;
17608
+ return undefined;
17770
17609
  };
17771
- registerCustomXPathFunction({
17772
- namespaceURI: XFORMS_NAMESPACE_URI,
17773
- localName: 'index'
17774
- }, ['xs:string?'], 'xs:integer?', (dynamicContext, string) => {
17775
- const {
17776
- formElement
17777
- } = dynamicContext.currentContext;
17778
- if (string === null) {
17779
- return 1;
17780
- }
17781
- const repeat = resolveId(string, formElement, 'fx-repeat');
17782
17610
 
17783
- // const def = instance.getInstanceData();
17784
- if (repeat) {
17785
- return repeat.getAttribute('index');
17611
+ /**
17612
+ * Resolve an id in scope. Behaves like the algorithm defined on https://www.w3.org/community/xformsusers/wiki/XForms_2.0#idref-resolve
17613
+ *
17614
+ * @param {string} id
17615
+ * @param {Node} sourceObject
17616
+ * @param {string} nodeName
17617
+ *
17618
+ * @returns {HTMLElement} The element with that ID, resolved with respect to repeats
17619
+ */
17620
+ function resolveId(id, sourceObject, nodeName = null) {
17621
+ const query = 'outermost(ancestor-or-self::fx-fore[1]/(descendant::fx-fore|descendant::*[@id = $id]))[not(self::fx-fore)]';
17622
+ /*
17623
+ if (nodeName === 'fx-instance') {
17624
+ // Instance elements can only be in the `model` element
17625
+ // query = 'ancestor-or-self::fx-fore[1]/fx-model/fx-instance[@id = $id]';
17626
+ const fore = Fore.getFore(sourceObject);
17627
+ const instances = fore.getModel().instances;
17628
+ const targetInstance = instances.find(i => i.id === id);
17629
+ return targetInstance;
17630
+ return document.getElementById(id);
17786
17631
  }
17787
- return Number(1);
17788
- });
17789
-
17790
- // Note that this is not to spec. The spec enforces elements to be returned from the
17791
- // instance. However, we allow instances to actually be JSON!
17792
- registerCustomXPathFunction({
17793
- namespaceURI: XFORMS_NAMESPACE_URI,
17794
- localName: 'instance'
17795
- }, [], 'item()?', domFacade => instance(domFacade, null));
17796
- registerCustomXPathFunction({
17797
- namespaceURI: XFORMS_NAMESPACE_URI,
17798
- localName: 'instance'
17799
- }, ['xs:string?'], 'item()?', instance);
17800
- const jsonToXml = (_dynamicContext, json) => {
17801
- const escapeXml = str => str.replace(/[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
17802
- const convert = (obj, parent) => {
17803
- const type = typeof obj;
17804
- if (type === 'number') {
17805
- parent.setAttribute('type', 'number');
17806
- parent.textContent = obj.toString();
17807
- } else if (type === 'boolean') {
17808
- parent.setAttribute('type', 'boolean');
17809
- parent.textContent = obj.toString();
17810
- } else if (obj === null) {
17811
- const node = document.createElement('_');
17812
- node.setAttribute('type', 'null');
17813
- parent.appendChild(node);
17814
- } else if (type === 'string') {
17815
- parent.textContent = escapeXml(obj);
17816
- } else if (Array.isArray(obj)) {
17817
- parent.setAttribute('type', 'array');
17818
- obj.forEach(item => {
17819
- const node = document.createElement('_');
17820
- convert(item, node);
17821
- node.textContent = item;
17822
- parent.appendChild(node);
17823
- });
17824
- } else if (type === 'object') {
17825
- parent.setAttribute('type', 'object');
17826
- Object.entries(obj).forEach(([key, value]) => {
17827
- if (value) {
17828
- const childNode = document.createElement(key.replace(/[^a-zA-Z0-9_]/g, '_'));
17829
- convert(value, childNode);
17830
- parent.appendChild(childNode);
17831
- }
17832
- });
17833
- }
17834
- };
17835
- const root = document.createElement('json');
17836
- if (Array.isArray(json)) {
17837
- root.setAttribute('type', 'array');
17838
- } else {
17839
- root.setAttribute('type', 'object');
17632
+ */
17633
+ if (sourceObject.nodeType === Node.TEXT_NODE) {
17634
+ sourceObject = sourceObject.parentNode;
17840
17635
  }
17841
- convert(json, root);
17842
- // return root.outerHTML;
17843
- console.log('xml', root);
17844
- return root;
17845
- };
17846
- registerCustomXPathFunction({
17847
- namespaceURI: XFORMS_NAMESPACE_URI,
17848
- localName: 'json2xml'
17849
- }, ['item()?'], 'item()?', jsonToXml);
17850
- const xmlToJson = (_dynamicContext, xml) => {
17851
- const isElementNode = node => node.nodeType === Node.ELEMENT_NODE;
17852
- const isTextNode = node => node.nodeType === Node.TEXT_NODE;
17853
- const parseNode = node => {
17854
- if (isElementNode(node)) {
17855
- const obj = {};
17856
- if (node.hasAttributes()) {
17857
- obj.type = node.getAttribute('type');
17858
- }
17859
- if (node.childNodes.length === 1 && isTextNode(node.firstChild)) {
17860
- return node.textContent;
17861
- }
17862
- for (const child of node.childNodes) {
17863
- const childName = child.nodeName;
17864
- const childValue = parseNode(child);
17865
- if (obj[childName]) {
17866
- if (!Array.isArray(obj[childName])) {
17867
- obj[childName] = [obj[childName]];
17868
- }
17869
- obj[childName].push(childValue);
17870
- } else {
17871
- obj[childName] = childValue;
17872
- }
17873
- }
17874
- return obj;
17636
+ if (sourceObject.nodeType === Node.ATTRIBUTE_NODE) {
17637
+ sourceObject = sourceObject.ownerElement;
17638
+ }
17639
+ if (sourceObject.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
17640
+ sourceObject = sourceObject.parentNode.host;
17641
+ }
17642
+ const ownerForm = sourceObject.localName === 'fx-fore' ? sourceObject : sourceObject.closest('fx-fore');
17643
+ const elementsWithId = ownerForm.querySelectorAll(`[id='${id}']`);
17644
+ if (elementsWithId.length === 1) {
17645
+ // A single one is found. Assume no ID reuse.
17646
+ const targetObject = elementsWithId[0];
17647
+ if (nodeName && targetObject.localName !== nodeName) {
17648
+ return null;
17875
17649
  }
17876
- if (isTextNode(node)) {
17877
- return node.textContent;
17650
+ return targetObject;
17651
+ }
17652
+ const allMatchingTargetObjects = evaluateXPathToNodes$1(query, sourceObject, null, {
17653
+ id
17654
+ }, {
17655
+ namespaceResolver: xhtmlNamespaceResolver
17656
+ });
17657
+ if (allMatchingTargetObjects.length === 0) {
17658
+ return null;
17659
+ }
17660
+ if (allMatchingTargetObjects.length === 1 && evaluateXPathToBoolean$1('(ancestor::fx-fore | ancestor::fx-repeat)[last()]/self::fx-fore', allMatchingTargetObjects[0], null, null, {
17661
+ namespaceResolver: xhtmlNamespaceResolver
17662
+ })) {
17663
+ // If the target element is not repeated, then the search for the target object is trivial since
17664
+ // there is only one associated with the target element that bears the matching ID. This is true
17665
+ // regardless of whether or not the source object is repeated. However, if the target element is
17666
+ // repeated, then additional information must be used to help select a target object from among
17667
+ // those associated with the identified target element.
17668
+ const targetObject = allMatchingTargetObjects[0];
17669
+ if (nodeName && targetObject.localName !== nodeName) {
17670
+ return null;
17878
17671
  }
17879
- return undefined;
17880
- };
17881
- const parser = new DOMParser();
17882
- const xmlDoc = parser.parseFromString(xml, 'application/xml');
17883
- const root = xmlDoc.documentElement;
17884
- return parseNode(root);
17885
- };
17886
- registerCustomXPathFunction({
17887
- namespaceURI: XFORMS_NAMESPACE_URI,
17888
- localName: 'xmltoJson'
17889
- }, ['item()?'], 'item()?', xmlToJson);
17672
+ return targetObject;
17673
+ }
17890
17674
 
17891
- /*
17892
- // Example usage:
17893
- const xml = '<json type="object"><given>Mark</given><family>Smith</family></json>';
17894
- console.log(xmlToJson(xml));
17895
- */
17675
+ // SPEC:
17896
17676
 
17897
- registerCustomXPathFunction({
17898
- namespaceURI: XFORMS_NAMESPACE_URI,
17899
- localName: 'depends'
17900
- }, ['node()*'], 'item()?', (_dynamicContext, nodes) =>
17901
- // console.log('depends on : ', nodes[0]);
17902
- nodes[0]);
17903
- registerCustomXPathFunction({
17904
- namespaceURI: XFORMS_NAMESPACE_URI,
17905
- localName: 'event'
17906
- }, ['xs:string?'], 'item()?', (dynamicContext, arg) => {
17907
- if (!arg) return null;
17908
- for (let ancestor = dynamicContext.currentContext.formElement; ancestor; ancestor = ancestor.parentNode) {
17909
- if (!ancestor.currentEvent) {
17910
- continue;
17911
- }
17677
+ // 12.2.1 References to Elements within a repeat Element
17912
17678
 
17913
- // We have a current event. read the property either from detail, or from the event
17914
- // itself.
17915
- // Check detail for custom events! This is how that is passed along
17916
- if (ancestor.currentEvent.detail && typeof ancestor.currentEvent.detail === 'object' && arg in ancestor.currentEvent.detail) {
17917
- return ancestor.currentEvent.detail[arg];
17918
- }
17679
+ // When the target element that is identified by the IDREF of a source object has one or more
17680
+ // repeat elements as ancestors, then the set of ancestor repeats are partitioned into two
17681
+ // subsets, those in common with the source element and those that are not in common. Any ancestor
17682
+ // repeat elements of the target element not in common with the source element are descendants of
17683
+ // the repeat elements that the source and target element have in common, if any.
17919
17684
 
17920
- // arg might be `code`, so currentEvent.code should work
17921
- if (arg.includes('.')) {
17922
- return _propertyLookup(ancestor.currentEvent, arg);
17685
+ // For the repeat elements that are in common, the desired target object exists in the same set of
17686
+ // run-time objects that contains the source object. Then, for each ancestor repeat of the target
17687
+ // element that is not in common with the source element, the current index of the repeat
17688
+ // determines the set of run-time objects that contains the desired target object.
17689
+ for (const ancestorRepeatItem of evaluateXPathToNodes$1('ancestor::fx-repeatitem => reverse()', sourceObject, null, null, {
17690
+ namespaceResolver: xhtmlNamespaceResolver
17691
+ })) {
17692
+ const foundTargetObjects = allMatchingTargetObjects.filter(to => XPathUtil.contains(ancestorRepeatItem, to));
17693
+ switch (foundTargetObjects.length) {
17694
+ case 0:
17695
+ // Nothing found: ignore
17696
+ break;
17697
+ case 1:
17698
+ {
17699
+ // A single one is found: the target object is directly in a common repeat
17700
+ const targetObject = foundTargetObjects[0];
17701
+ if (nodeName && targetObject.localName !== nodeName) {
17702
+ return null;
17703
+ }
17704
+ return targetObject;
17705
+ }
17706
+ default:
17707
+ {
17708
+ // Multiple target objects are found: they are in a repeat that is not common with the
17709
+ // source object We found a target object in a common repeat! We now need to find the one
17710
+ // that is in the repeatitem identified at the current index
17711
+ const targetObject = foundTargetObjects.find(to => evaluateXPathToNodes$1('every $ancestor of ancestor::fx-repeatitem satisfies $ancestor is $ancestor/../child::fx-repeatitem[../@repeat-index]', to, null, {}));
17712
+ if (!targetObject) {
17713
+ // Nothing valid found for whatever reason. This might be something dynamic?
17714
+ return null;
17715
+ }
17716
+ if (nodeName && targetObject.localName !== nodeName) {
17717
+ return null;
17718
+ }
17719
+ return targetObject;
17720
+ }
17923
17721
  }
17924
- return ancestor.currentEvent[arg] || null;
17925
17722
  }
17723
+ // We found no target objects in common repeats. The id is unresolvable
17926
17724
  return null;
17927
- });
17928
- function _propertyLookup(obj, path) {
17929
- const parts = path.split('.');
17930
- if (parts.length == 1) {
17931
- return obj[parts[0]];
17932
- }
17933
- return _propertyLookup(obj[parts[0]], parts.slice(1).join('.'));
17934
17725
  }
17935
17726
 
17936
- // Implement the XForms standard functions here.
17937
- registerXQueryModule(`
17938
- module namespace xf="${XFORMS_NAMESPACE_URI}";
17939
-
17940
- declare %public function xf:boolean-from-string($str as xs:string) as xs:boolean {
17941
- lower-case($str) = "true" or $str = "1"
17942
- };
17943
- `);
17944
-
17945
- // How to run XQUERY:
17946
- /**
17947
- registerXQueryModule(`
17948
- module namespace my-custom-namespace = "my-custom-uri";
17949
- (:~
17950
- Insert attribute somewhere
17951
- ~:)
17952
- declare %public %updating function my-custom-namespace:do-something ($ele as element()) as xs:boolean {
17953
- if ($ele/@done) then false() else
17954
- (insert node
17955
- attribute done {"true"}
17956
- into $ele, true())
17957
- };
17958
- `)
17959
- // At some point:
17960
- const contextNode = null;
17961
- const pendingUpdatesAndXdmValue = evaluateUpdatingExpressionSync('ns:do-something(.)', contextNode, null, null, {moduleImports: {'ns': 'my-custom-uri'}})
17962
-
17963
- console.log(pendingUpdatesAndXdmValue.xdmValue); // this is true or false, see function
17964
-
17965
- executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null, null);
17966
- */
17967
-
17727
+ // Make namespace resolving use the `instance` element that is related to here
17728
+ const xmlDocument = new DOMParser().parseFromString('<xml />', 'text/xml');
17729
+ const instanceReferencesByQuery = new Map();
17730
+ function findInstanceReferences(xpathQuery) {
17731
+ if (!xpathQuery.includes('instance')) {
17732
+ // No call to the instance function anyway: short-circuit and prevent AST processing
17733
+ return [];
17734
+ }
17735
+ if (instanceReferencesByQuery.has(xpathQuery)) {
17736
+ return instanceReferencesByQuery.get(xpathQuery);
17737
+ }
17738
+ const xpathAST = parseScript(xpathQuery, {}, xmlDocument);
17739
+ const instanceReferences = evaluateXPathToStrings$1(`descendant::xqx:functionCallExpr
17740
+ [xqx:functionName = "instance"]
17741
+ /xqx:arguments
17742
+ /xqx:stringConstantExpr
17743
+ /xqx:value`, xpathAST, null, {}, {
17744
+ namespaceResolver: prefix => prefix === 'xqx' ? 'http://www.w3.org/2005/XQueryX' : undefined
17745
+ });
17746
+ instanceReferencesByQuery.set(xpathQuery, instanceReferences);
17747
+ return instanceReferences;
17748
+ }
17968
17749
  /**
17969
- * @param input as string
17970
- * @return {string}
17750
+ * @typedef {function(string):string} NamespaceResolver
17971
17751
  */
17972
- registerCustomXPathFunction({
17973
- namespaceURI: XFORMS_NAMESPACE_URI,
17974
- localName: 'base64encode'
17975
- }, ['xs:string?'], 'xs:string?', (_dynamicContext, string) => btoa(string));
17976
- registerCustomXPathFunction({
17977
- namespaceURI: XFORMS_NAMESPACE_URI,
17978
- localName: 'local-date'
17979
- }, [], 'xs:string?', (_dynamicContext, _string) => new Date().toLocaleDateString());
17980
- registerCustomXPathFunction({
17981
- namespaceURI: XFORMS_NAMESPACE_URI,
17982
- localName: 'local-dateTime'
17983
- }, [], 'xs:string?', (_dynamicContext, _string) => new Date().toLocaleString());
17984
- registerCustomXPathFunction({
17985
- namespaceURI: XFORMS_NAMESPACE_URI,
17986
- localName: 'uri'
17987
- }, [], 'xs:string?', (_dynamicContext, _string) => window.location.href);
17988
- registerCustomXPathFunction({
17989
- namespaceURI: XFORMS_NAMESPACE_URI,
17990
- localName: 'uri-fragment'
17991
- }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.hash);
17992
- registerCustomXPathFunction({
17993
- namespaceURI: XFORMS_NAMESPACE_URI,
17994
- localName: 'uri-host'
17995
- }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.host);
17996
- registerCustomXPathFunction({
17997
- namespaceURI: XFORMS_NAMESPACE_URI,
17998
- localName: 'uri-query'
17999
- }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.search);
18000
- registerCustomXPathFunction({
18001
- namespaceURI: XFORMS_NAMESPACE_URI,
18002
- localName: 'uri-relpath'
18003
- }, [], 'xs:string?', (_dynamicContext, _arg) => {
18004
- const path = new URL(window.location.href).pathname;
18005
- return path.substring(0, path.lastIndexOf('/') + 1);
18006
- });
18007
- registerCustomXPathFunction({
18008
- namespaceURI: XFORMS_NAMESPACE_URI,
18009
- localName: 'uri-path'
18010
- }, [], 'xs:string?', (_dynamicContext, _arg) => new URL(window.location.href).pathname);
18011
- registerCustomXPathFunction({
18012
- namespaceURI: XFORMS_NAMESPACE_URI,
18013
- localName: 'uri-port'
18014
- }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.port);
18015
- registerCustomXPathFunction({
18016
- namespaceURI: XFORMS_NAMESPACE_URI,
18017
- localName: 'uri-param'
18018
- }, ['xs:string?'], 'xs:string?', (_dynamicContext, arg) => {
18019
- if (!arg) return null;
18020
- const {
18021
- search
18022
- } = window.location;
18023
- const urlparams = new URLSearchParams(search);
18024
- const param = urlparams.get(arg);
18025
- return param || '';
18026
- });
18027
- registerCustomXPathFunction({
18028
- namespaceURI: XFORMS_NAMESPACE_URI,
18029
- localName: 'uri-scheme'
18030
- }, [], 'xs:string?', (_dynamicContext, _arg) => new URL(window.location.href).protocol);
18031
- registerCustomXPathFunction({
18032
- namespaceURI: XFORMS_NAMESPACE_URI,
18033
- localName: 'uri-scheme-specific-part'
18034
- }, [], 'xs:string?', (_dynamicContext, _arg) => {
18035
- const uri = window.location.href;
18036
- return uri.substring(uri.indexOf(':') + 1, uri.length);
18037
- });
18038
17752
 
18039
17753
  /**
18040
- * @param {Node} node
18041
- * @returns string
17754
+ * @function
17755
+ * Resolve a namespace. Needs a namespace prefix and the element that is most closely related to the
17756
+ * XPath in which the namespace is being resolved. The prefix will be resolved by using the
17757
+ * ancestry of said element.
17758
+ *
17759
+ * It has two ways of doing so:
17760
+ *
17761
+ * - If the prefix is defined in an `xmlns:XXX="YYY"` namespace declaration, it will return 'YYY'.
17762
+ * - If the prefix is the empty prefix and there is an `xpath-default-namespace="YYY"` attribute in
17763
+ * - the * ancestry, that attribute will be used and 'YYY' will be returned
17764
+ *
17765
+ * @param {string} xpathQuery
17766
+ * @param {HTMLElement} formElement
17767
+ * @returns {NamespaceResolver} The namespace resolver for this context
18042
17768
  */
18043
- /*
18044
- export static getDocPath(node) {
18045
- const path = fx.evaluateXPathToString('path()', node);
18046
- // Path is like `$default/x[1]/y[1]`
18047
- const shortened = XPathUtil.shortenPath(path);
18048
- return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
18049
- }
18050
- */
18051
-
18052
- class XPathUtil {
18053
- /**
18054
- * Recursively check AST for any dynamic expression components.
18055
- */
18056
- static containsDynamicContent(astNode) {
18057
- if (!astNode) return false;
18058
-
18059
- // Location paths, function calls, or variable refs are dynamic
18060
- if (astNode.type === 'pathExpression' || astNode.type === 'functionCall' || astNode.type === 'variableReference') {
18061
- return true;
17769
+ function createNamespaceResolver(xpathQuery, formElement) {
17770
+ const cachedResolver = getCachedNamespaceResolver(xpathQuery, formElement);
17771
+ if (cachedResolver) {
17772
+ return cachedResolver;
17773
+ }
17774
+ let instanceReferences = findInstanceReferences(xpathQuery);
17775
+ if (instanceReferences.length === 0) {
17776
+ // No instance functions. Look up further in the hierarchy to see if we can deduce the intended context from there
17777
+ const ancestorComponent = formElement.parentNode && formElement.parentNode.nodeType === formElement.ELEMENT_NODE && formElement.parentNode.closest('[ref]');
17778
+ if (ancestorComponent) {
17779
+ const resolver = createNamespaceResolver(ancestorComponent.getAttribute('ref'), ancestorComponent);
17780
+ setCachedNamespaceResolver(xpathQuery, formElement, resolver);
17781
+ return resolver;
18062
17782
  }
18063
-
18064
- // Recursively check any child expressions
18065
- for (const key in astNode) {
18066
- if (astNode[key] && typeof astNode[key] === 'object') {
18067
- if (Array.isArray(astNode[key])) {
18068
- for (const item of astNode[key]) {
18069
- if (XPathUtil.containsDynamicContent(item)) return true;
18070
- }
18071
- } else {
18072
- if (XPathUtil.containsDynamicContent(astNode[key])) return true;
17783
+ // Nothing found: let's just assume we're supposed to use the `default` instance
17784
+ instanceReferences = ['default'];
17785
+ }
17786
+ if (instanceReferences.length === 1) {
17787
+ // console.log(`resolving ${xpathQuery} with ${instanceReferences[0]}`);
17788
+ let instance;
17789
+ if (instanceReferences[0] === 'default') {
17790
+ /**
17791
+ * @type {HTMLElement}
17792
+ */
17793
+ const actualForeElement = evaluateXPathToFirstNode$1('ancestor-or-self::fx-fore[1]', formElement, null, null, {
17794
+ namespaceResolver: xhtmlNamespaceResolver
17795
+ });
17796
+ instance = actualForeElement && actualForeElement.querySelector('fx-instance');
17797
+ } else {
17798
+ instance = resolveId(instanceReferences[0], formElement, 'fx-instance');
17799
+ }
17800
+ if (instance && instance.hasAttribute('xpath-default-namespace')) {
17801
+ const xpathDefaultNamespace = instance.getAttribute('xpath-default-namespace');
17802
+ /*
17803
+ console.log(
17804
+ `Resolving the xpath ${xpathQuery} with the default namespace set to ${xpathDefaultNamespace}`,
17805
+ );
17806
+ */
17807
+ /**
17808
+ * @type {NamespaceResolver}
17809
+ */
17810
+ const resolveNamespacePrefix = prefix => {
17811
+ if (!prefix) {
17812
+ return xpathDefaultNamespace;
18073
17813
  }
18074
- }
17814
+ return undefined;
17815
+ };
17816
+ setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
17817
+ return resolveNamespacePrefix;
18075
17818
  }
18076
- return false;
18077
17819
  }
17820
+ /*
17821
+ if (instanceReferences.length > 1) {
17822
+ console.warn(
17823
+ `More than one instance is used in the query "${xpathQuery}". The default namespace resolving will be used`,
17824
+ );
17825
+ }
17826
+ */
17827
+
17828
+ const xpathDefaultNamespace = evaluateXPathToString$1('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) || '';
18078
17829
 
18079
17830
  /**
18080
- * creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
18081
- * nodes.
18082
- * E.g. 'foo/bar' creates an element 'foo' with an child element 'bar'
18083
- * 'foo/@bar' creates a 'foo' element with an 'bar' attribute
18084
- *
18085
- * supports multiple steps
18086
- *
18087
- * @param xpath
18088
- * @param doc {XMLDocument}
18089
- * @param fore
18090
- * @return {Node|Attr}
17831
+ * @type {NamespaceResolver}
18091
17832
  */
18092
- static createNodesFromXPath(xpath, doc, fore) {
18093
- const resolveNamespace = createNamespaceResolver(xpath, fore);
18094
- if (!doc) {
18095
- doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
18096
- }
18097
-
18098
- const parts = [];
18099
- let scratch = '';
18100
- let isInPredicate = false;
18101
- for (const char of xpath.split('')) {
18102
- if (!isInPredicate) {
18103
- // We are not in a predicate, the slash will terminate our step.
18104
- if (char === '/') {
18105
- parts.push(scratch);
18106
- scratch = '';
18107
- continue;
18108
- }
18109
- scratch += char;
18110
- if (char === '[') {
18111
- isInPredicate = true;
18112
- }
18113
- continue;
18114
- }
18115
- // We are in a predicate! So the only interesting token is ']', which means we're out of one.
18116
- scratch += char;
18117
- if (char === ']') {
18118
- isInPredicate = false;
18119
- }
17833
+ const resolveNamespacePrefix = function resolveNamespacePrefix(prefix) {
17834
+ if (prefix === '') {
17835
+ return xpathDefaultNamespace;
18120
17836
  }
18121
- // Flush the last step
18122
- parts.push(scratch);
18123
- let rootNode = null;
18124
- let currentNode = null;
18125
- for (const part of parts) {
18126
- if (!part) continue; // Skip empty parts (e.g., leading slashes)
18127
- if (part === '.') {
18128
- // A '.' does not introduce new elements
18129
- continue;
18130
- }
18131
17837
 
18132
- // Handle attributes
18133
- if (part.startsWith('@')) {
18134
- const attrName = part.slice(1); // Strip '@'
18135
- if (!currentNode) {
18136
- return doc.createAttribute(attrName, '');
18137
- }
18138
- currentNode.setAttribute(attrName, '');
18139
- } else {
18140
- // We are a predicate selector! Handle it
18141
- // This regex matches strings like:
18142
- // - listBibl
18143
- // - tei:listBibl
18144
- // - listBibl[@type="foo"]
18145
- // - listBibl[@type="foo"][@class="bar"]
18146
- // It will also match strings like
18147
- // - listBibl[ancestor-or-self::foo]
18148
- // which will be filtered out later.
17838
+ // Note: ideally we should use Node#lookupNamespaceURI. However, the nodes we are passed are
17839
+ // XML. The best we can do is emulate the `xmlns:xxx` namespace declarations by regarding them as
17840
+ // attributes. Which they technically ARE NOT!
18149
17841
 
18150
- const result = part.match(/^(?<name>[\w:-]+)(?<predicates>(\[[^]*\])*)$/);
18151
- if (!result) {
18152
- throw new Error(`No element could be made from the XPath step ${part}. It must be of these forms: 'localName', 'prefix:name', 'name[@attr="value"]' et cetera.`);
18153
- }
18154
- const {
18155
- name,
18156
- predicates
18157
- } = result.groups;
18158
- // Handle namespaces if present
18159
- const [prefix, localName] = name.includes(':') ? name.split(':') : [null, name];
18160
- const namespace = resolveNamespace(prefix);
18161
- const newElement = namespace ? doc.createElementNS(namespace, localName) : doc.createElement(localName);
18162
- if (predicates) {
18163
- const predicateExtractionRegex = /(\[@(?<name>[\w:-]*)\s?=\s?["'](?<value>[^"']*)['"]\])+/g;
18164
- const parsedPredicates = predicates.matchAll(predicateExtractionRegex).map(match => ({
18165
- attrName: match.groups.name,
18166
- value: match.groups.value
18167
- }));
18168
- for (const {
18169
- attrName,
18170
- value
18171
- } of parsedPredicates) {
18172
- newElement.setAttribute(attrName, value);
18173
- }
18174
- }
18175
- if (!rootNode) {
18176
- rootNode = newElement; // Set as the root node
18177
- } else {
18178
- currentNode.appendChild(newElement);
18179
- }
18180
- currentNode = newElement;
18181
- }
18182
- }
18183
- if (!rootNode) {
18184
- throw new Error('Invalid XPath; no root element could be created.');
18185
- }
18186
- return rootNode;
17842
+ return evaluateXPathToString$1('ancestor-or-self::*/@*[name() = "xmlns:" || $prefix][last()]', formElement, null, {
17843
+ prefix
17844
+ });
17845
+ };
17846
+ setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
17847
+ return resolveNamespacePrefix;
17848
+ }
17849
+ function createNamespaceResolverForNode(query, contextNode, formElement) {
17850
+ if ((contextNode && contextNode.ownerDocument || contextNode) === window.document) {
17851
+ // Running a query on the HTML DOM. Don't bother resolving namespaces in any other way
17852
+ return xhtmlNamespaceResolver;
18187
17853
  }
17854
+ return createNamespaceResolver(query, formElement);
17855
+ }
18188
17856
 
18189
- /**
18190
- * looks up namespace on ownerForm. Though not strictly in the sense of resolving namespaces in XML, the
18191
- * fx-fore element is a convenient place to put namespace declarations for 2 reasons:
18192
- * - this way namespaces are scoped to a Fore element
18193
- * - as fx-fore is a web component we can add our xmlns attributes as we got no restrictions to attributes
18194
- * though strictly speaking they are no xmlns declarations and just serve the purpose of namespace lookup.
18195
- *
18196
- * @param boundElement
18197
- * @param prefix
18198
- * @return {string}
18199
- */
18200
- static lookupNamespace(ownerForm, prefix) {
18201
- return ownerForm.getAttribute(`xmlns:${prefix}`);
18202
- }
18203
- static querySelectorAll(querySelector, start) {
18204
- const queue = [start];
18205
- const found = [];
18206
- while (queue.length) {
18207
- const item = queue.shift();
18208
- for (const child of Array.from(item.children).reverse()) {
18209
- queue.unshift(child);
17857
+ /**
17858
+ * Implementation of the functionNameResolver passed to FontoXPath to
17859
+ * redirect function resolving for unprefixed functions to either the fn or the xf namespace
17860
+ */
17861
+ // eslint-disable-next-line no-unused-vars
17862
+ function functionNameResolver({
17863
+ prefix,
17864
+ localName
17865
+ }, _arity) {
17866
+ switch (localName) {
17867
+ // TODO: put the full XForms library functions set here
17868
+ case 'context':
17869
+ case 'base64encode':
17870
+ case 'boolean-from-string':
17871
+ case 'current':
17872
+ case 'depends':
17873
+ case 'event':
17874
+ case 'fore-attr':
17875
+ case 'index':
17876
+ case 'instance':
17877
+ case 'json2xml':
17878
+ case 'xml2Json':
17879
+ case 'log':
17880
+ case 'parse':
17881
+ case 'local-date':
17882
+ case 'local-dateTime':
17883
+ case 'logtree':
17884
+ case 'uri':
17885
+ case 'uri-fragment':
17886
+ case 'uri-host':
17887
+ case 'uri-param':
17888
+ case 'uri-path':
17889
+ case 'uri-relpath':
17890
+ case 'uri-port':
17891
+ case 'uri-query':
17892
+ case 'uri-scheme':
17893
+ case 'uri-scheme-specific-part':
17894
+ return {
17895
+ namespaceURI: XFORMS_NAMESPACE_URI,
17896
+ localName
17897
+ };
17898
+ default:
17899
+ if (prefix === '' && globallyDeclaredFunctionLocalNames.includes(localName)) {
17900
+ // The function has been declared without a prefix and is called here without a prefix.
17901
+ // Just make this work. It is the developer-friendly way
17902
+ return {
17903
+ namespaceURI: 'http://www.w3.org/2005/xquery-local-functions',
17904
+ localName
17905
+ };
18210
17906
  }
18211
- if (item.matches && item.matches('template')) {
18212
- queue.unshift(item.content);
17907
+ if (prefix === 'fn' || prefix === '') {
17908
+ return {
17909
+ namespaceURI: 'http://www.w3.org/2005/xpath-functions',
17910
+ localName
17911
+ };
18213
17912
  }
18214
- if (item.matches && item.matches(querySelector)) {
18215
- found.push(item);
17913
+ if (prefix === 'local') {
17914
+ return {
17915
+ namespaceURI: 'http://www.w3.org/2005/xquery-local-functions',
17916
+ localName
17917
+ };
18216
17918
  }
18217
- }
18218
- return found;
17919
+ return null;
18219
17920
  }
17921
+ }
18220
17922
 
18221
- /**
18222
- * Alternative to `contains` that respects shadowroots
18223
- * @param {Node} ancestor
18224
- * @param {Node} descendant
18225
- * @returns {boolean}
18226
- */
18227
- static contains(ancestor, descendant) {
18228
- while (descendant) {
18229
- if (descendant === ancestor) {
18230
- return true;
18231
- }
18232
- if (descendant.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
18233
- // We are passing a shadow root boundary
18234
- descendant = descendant.host;
18235
- } else {
18236
- descendant = descendant.parentNode;
18237
- }
18238
- }
18239
- return false;
17923
+ /**
17924
+ * Get the variables in scope of the form element. These are the values of the variables that
17925
+ * logically precede the formElement that declares the XPath
17926
+ *
17927
+ * @param {Node} formElement The element that declares the XPath
17928
+ *
17929
+ * @returns {Object} A key-value mapping of the variables
17930
+ */
17931
+ function getVariablesInScope(formElement) {
17932
+ let closestActualFormElement = formElement;
17933
+ while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
17934
+ closestActualFormElement = closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE ? closestActualFormElement.ownerElement : closestActualFormElement.parentNode;
18240
17935
  }
18241
-
18242
- /**
18243
- * Alternative to `closest` that respects subcontrol boundaries
18244
- *
18245
- * @param {string} querySelector
18246
- * @param {Node} start
18247
- * @returns {HTMLElement}
18248
- */
18249
- static getClosest(querySelector, start) {
18250
- while (start && !start.matches || !start.matches(querySelector)) {
18251
- if (start.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
18252
- // We are passing a shadow root boundary
18253
- start = start.host;
18254
- continue;
18255
- }
18256
- if (start.nodeType === Node.ATTRIBUTE_NODE) {
18257
- // We are passing an attribute
18258
- start = start.ownerElement;
17936
+ if (!closestActualFormElement) {
17937
+ return {};
17938
+ }
17939
+ const variables = {};
17940
+ if (closestActualFormElement.inScopeVariables) {
17941
+ for (const key of closestActualFormElement.inScopeVariables.keys()) {
17942
+ const varElementOrValue = closestActualFormElement.inScopeVariables.get(key);
17943
+ if (!varElementOrValue) {
18259
17944
  continue;
18260
17945
  }
18261
- if (start.nodeType === Node.TEXT_NODE) {
18262
- start = start.parentNode;
18263
- }
18264
- if (start.matches('fx-fore')) {
18265
- // Subform reached. Bail out
18266
- return null;
18267
- }
18268
- start = start.parentNode;
18269
- if (!start) {
18270
- return null;
17946
+ if (varElementOrValue.nodeType) {
17947
+ // We are a var element, set the value to the value computed there
17948
+ variables[key] = varElementOrValue.value;
17949
+ // variables[key] = varElementOrValue.inScopeVariables.get(key);
17950
+ } else {
17951
+ // We are a direct value. This is used to leak in event variables
17952
+ variables[key] = varElementOrValue;
18271
17953
  }
18272
17954
  }
18273
- return start;
18274
17955
  }
17956
+ return variables;
17957
+ }
18275
17958
 
18276
- /**
18277
- * returns next bound element upwards in tree
18278
- * @param {Node} start where to start the search
18279
- * @returns {*|null}
18280
- */
18281
- static getParentBindingElement(start) {
18282
- /* if (start.parentNode.host) {
18283
- const { host } = start.parentNode;
18284
- if (host.hasAttribute('ref')) {
18285
- return host;
18286
- }
18287
- } else */
18288
- if (start.parentNode && (start.parentNode.nodeType !== Node.DOCUMENT_NODE || start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)) {
18289
- return this.getClosest('fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref],fx-repeatitem', start.parentNode);
18290
- }
18291
- return null;
18292
- }
17959
+ /**
17960
+ * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
17961
+ * type of the output is more predictable.
17962
+ *
17963
+ * @param {string} xpath The XPath to run
17964
+ * @param {Node} contextNode The start of the XPath
17965
+ * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
17966
+ * @param {Object} variables Any variables to pass to the XPath
17967
+ * @param {Object} options Any options to pass to the XPath
17968
+ *
17969
+ * @returns {any[]}
17970
+ */
17971
+ /*
17972
+ export function evaluateXPath(xpath, contextNode, formElement, variables = {}, options={}, domFacade = null) {
17973
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17974
+ const variablesInScope = getVariablesInScope(formElement);
18293
17975
 
18294
- /**
18295
- * Checks whether the specified path expression is an absolute path.
18296
- *
18297
- * @param {string} path the path expression.
18298
- * @returns {boolean} <code>true</code> if specified path expression is an absolute
18299
- * path, otherwise <code>false</code>.
18300
- */
18301
- static isAbsolutePath(path) {
18302
- return path != null && (path.startsWith('/') || path.startsWith('instance(') || path.startsWith('$'));
18303
- }
17976
+ return fxEvaluateXPath(
17977
+ xpath,
17978
+ contextNode,
17979
+ domFacade,
17980
+ {...variablesInScope, ...variables},
17981
+ fxEvaluateXPath.ALL_RESULTS_TYPE,
17982
+ {
17983
+ debug: true,
17984
+ currentContext: {formElement, variables},
17985
+ moduleImports: {
17986
+ xf: XFORMS_NAMESPACE_URI,
17987
+ },
17988
+ functionNameResolver,
17989
+ namespaceResolver,
17990
+ language: options.language || evaluateXPath.XPATH_3_1
17991
+ },
17992
+ );
17993
+ }
17994
+ */
17995
+ function evaluateXPath(xpath, contextNode, formElement, variables = {}, options = {}) {
17996
+ try {
17997
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17998
+ const variablesInScope = getVariablesInScope(formElement);
17999
+ const result = evaluateXPath$1(xpath, contextNode, null, {
18000
+ ...variablesInScope,
18001
+ ...variables
18002
+ }, evaluateXPath$1.ALL_RESULTS_TYPE, {
18003
+ xmlSerializer: new XMLSerializer(),
18004
+ debug: true,
18005
+ currentContext: {
18006
+ formElement,
18007
+ variables
18008
+ },
18009
+ moduleImports: {
18010
+ xf: XFORMS_NAMESPACE_URI
18011
+ },
18012
+ functionNameResolver,
18013
+ namespaceResolver,
18014
+ language: options.language || evaluateXPath$1.XPATH_3_1_LANGUAGE
18015
+ });
18016
+ // console.log('evaluateXPath',xpath, result);
18017
+ return result;
18018
+ } catch (e) {
18019
+ formElement.dispatchEvent(new CustomEvent('error', {
18020
+ composed: false,
18021
+ bubbles: true,
18022
+ detail: {
18023
+ origin: formElement,
18024
+ message: `Expression '${xpath}' failed: ${e}`,
18025
+ expr: xpath,
18026
+ level: 'Error'
18027
+ }
18028
+ }));
18304
18029
 
18305
- /**
18306
- * @param {string} ref
18307
- */
18308
- static isSelfReference(ref) {
18309
- return ref === '.' || ref === './text()' || ref === 'text()' || ref === '' || ref === null;
18030
+ /*
18031
+ formElement.dispatchEvent(
18032
+ new CustomEvent('error', {
18033
+ composed: false,
18034
+ bubbles: true,
18035
+ cancelable:true,
18036
+ detail: {
18037
+ origin: formElement,
18038
+ message: `Expression '${xpath}' failed`,
18039
+ expr:xpath,
18040
+ level:'Error'},
18041
+ }),
18042
+ );
18043
+ */
18044
+ // Return 'nothing' in hope the rest of the page can forgive this
18045
+ return [];
18310
18046
  }
18311
-
18312
- /**
18313
- * returns the instance id from a complete XPath using `instance()` function.
18314
- *
18315
- * Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
18316
- *
18317
- * Otherwise instance id is extracted from function and returned. If all fails null is returned.
18318
- * @param {string} ref
18319
- * @param {HTMLElement} boundElement The element related to this ref. Used to resolve variables
18320
- * @returns {string}
18321
- */
18322
- static getInstanceId(ref, boundElement) {
18323
- if (!ref) {
18324
- return 'default';
18325
- }
18326
- if (ref.startsWith('instance()')) {
18327
- return 'default';
18328
- }
18329
- if (ref.startsWith('instance(')) {
18330
- const result = ref.substring(ref.indexOf('(') + 1);
18331
- return result.substring(1, result.indexOf(')') - 1);
18332
- }
18333
- if (ref.startsWith('$')) {
18334
- // this variable might actually point to an instance
18335
- const variableName = ref.match(/\$(?<variableName>[a-zA-Z0-9\-\_]+).*/)?.groups?.variableName;
18336
- let closestActualFormElement = boundElement;
18337
- while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
18338
- closestActualFormElement = closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE ? closestActualFormElement.ownerElement : closestActualFormElement.parentNode;
18339
- }
18340
- const correspondingVariable = closestActualFormElement?.inScopeVariables?.get(variableName);
18341
- if (!correspondingVariable) {
18342
- return null;
18047
+ }
18048
+ /**
18049
+ * Evaluate an XPath to the first Node
18050
+ *
18051
+ * @param {string} xpath The XPath to run
18052
+ * @param {Node} contextNode The start of the XPath
18053
+ * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
18054
+ * @returns {Node} The first node found in the XPath
18055
+ */
18056
+ function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
18057
+ try {
18058
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
18059
+ const variablesInScope = getVariablesInScope(formElement);
18060
+ const result = evaluateXPathToFirstNode$1(xpath, contextNode, null, variablesInScope, {
18061
+ defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
18062
+ moduleImports: {
18063
+ xf: XFORMS_NAMESPACE_URI
18064
+ },
18065
+ currentContext: {
18066
+ formElement
18067
+ },
18068
+ functionNameResolver,
18069
+ namespaceResolver,
18070
+ xmlSerializer: new XMLSerializer()
18071
+ });
18072
+ // console.log('evaluateXPathToFirstNode',xpath, result);
18073
+ return result;
18074
+ } catch (e) {
18075
+ formElement.dispatchEvent(new CustomEvent('error', {
18076
+ composed: false,
18077
+ bubbles: true,
18078
+ detail: {
18079
+ origin: formElement,
18080
+ message: `Expression '${xpath}' failed: ${e}`,
18081
+ expr: xpath,
18082
+ level: 'Error'
18343
18083
  }
18344
- return this.getInstanceId(correspondingVariable.valueQuery, correspondingVariable);
18345
- }
18346
- return null;
18347
- }
18348
-
18349
- /**
18350
- * @param {HTMLElement} boundElement
18351
- * @param {string} path
18352
- * @returns {string}
18353
- */
18354
- static resolveInstance(boundElement, path) {
18355
- let instanceId = XPathUtil.getInstanceId(path, boundElement);
18356
- if (!instanceId) {
18357
- instanceId = XPathUtil.getInstanceId(boundElement.getAttribute('ref'), boundElement);
18358
- }
18359
- if (instanceId !== null) {
18360
- return instanceId;
18361
- }
18362
- const parentBinding = XPathUtil.getParentBindingElement(boundElement);
18363
- if (parentBinding) {
18364
- return this.resolveInstance(parentBinding, path);
18365
- }
18366
- return 'default';
18084
+ }));
18367
18085
  }
18086
+ return null;
18087
+ }
18368
18088
 
18369
- /**
18370
- * @param {Node} node
18371
- * @returns string
18372
- */
18373
- /*
18374
- static getDocPath(node) {
18375
- const path = fx.evaluateXPathToString('path()', node);
18376
- // Path is like `$default/x[1]/y[1]`
18377
- const shortened = XPathUtil.shortenPath(path);
18378
- return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
18089
+ /**
18090
+ * Evaluate an XPath to all nodes
18091
+ *
18092
+ * @param {string} xpath The XPath to run
18093
+ * @param {Node} contextNode The start of the XPath
18094
+ * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
18095
+ * @return {Node[]} All nodes
18096
+ */
18097
+ function evaluateXPathToNodes(xpath, contextNode, formElement) {
18098
+ try {
18099
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
18100
+ const variablesInScope = getVariablesInScope(formElement);
18101
+ const result = evaluateXPathToNodes$1(xpath, contextNode, null, variablesInScope, {
18102
+ currentContext: {
18103
+ formElement
18104
+ },
18105
+ functionNameResolver,
18106
+ moduleImports: {
18107
+ xf: XFORMS_NAMESPACE_URI
18108
+ },
18109
+ namespaceResolver,
18110
+ xmlSerializer: new XMLSerializer()
18111
+ });
18112
+ // console.log('evaluateXPathToNodes',xpath, result);
18113
+ return result;
18114
+ } catch (e) {
18115
+ formElement.dispatchEvent(new CustomEvent('error', {
18116
+ composed: false,
18117
+ bubbles: true,
18118
+ detail: {
18119
+ origin: formElement,
18120
+ message: `Expression '${xpath}' failed: ${e}`,
18121
+ expr: xpath,
18122
+ level: 'Error'
18123
+ }
18124
+ }));
18379
18125
  }
18380
- */
18126
+ }
18381
18127
 
18382
- /**
18383
- * @param {Node} node
18384
- * @param {string} instanceId
18385
- * @returns string
18386
- */
18387
- static getPath(node, instanceId) {
18388
- const path = evaluateXPathToString$1('path()', node);
18389
- // Path is like `$default/x[1]/y[1]`
18390
- const shortened = XPathUtil.shortenPath(path);
18391
- return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
18128
+ /**
18129
+ * Evaluate an XPath to a boolean
18130
+ *
18131
+ * @param {string} xpath The XPath to run
18132
+ * @param {Node} contextNode The start of the XPath
18133
+ * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
18134
+ * @return {boolean}
18135
+ */
18136
+ function evaluateXPathToBoolean(xpath, contextNode, formElement) {
18137
+ try {
18138
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
18139
+ const variablesInScope = getVariablesInScope(formElement);
18140
+ return evaluateXPathToBoolean$1(xpath, contextNode, null, variablesInScope, {
18141
+ currentContext: {
18142
+ formElement
18143
+ },
18144
+ functionNameResolver,
18145
+ moduleImports: {
18146
+ xf: XFORMS_NAMESPACE_URI
18147
+ },
18148
+ namespaceResolver,
18149
+ xmlSerializer: new XMLSerializer()
18150
+ });
18151
+ } catch (e) {
18152
+ formElement.dispatchEvent(new CustomEvent('error', {
18153
+ composed: false,
18154
+ bubbles: true,
18155
+ detail: {
18156
+ origin: formElement,
18157
+ message: `Expression '${xpath}' failed: ${e}`,
18158
+ expr: xpath,
18159
+ level: 'Error'
18160
+ }
18161
+ }));
18392
18162
  }
18163
+ }
18393
18164
 
18394
- /**
18395
- * @param {string} path
18396
- * @returns string
18397
- */
18398
- static shortenPath(path) {
18399
- const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
18400
- if (tmp === 'root()') return tmp;
18401
- // cut off leading slash
18402
- const tmp1 = tmp.substring(1, tmp.length);
18403
- // ### cut-off root node ref
18404
- return tmp1.substring(tmp1.indexOf('/'), tmp.length);
18165
+ /**
18166
+ * Evaluate an XPath to a string
18167
+ *
18168
+ * @param {string} xpath The XPath to run
18169
+ * @param {Node} contextNode The start of the XPath
18170
+ * @param {Node} formElement The form element associated to the XPath
18171
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
18172
+ * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
18173
+ * access. This is used to determine dependencies between bind elements.
18174
+ * @return {string}
18175
+ */
18176
+ function evaluateXPathToString(xpath, contextNode, formElement, domFacade = null) {
18177
+ try {
18178
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
18179
+ const variablesInScope = getVariablesInScope(formElement);
18180
+ return evaluateXPathToString$1(xpath, contextNode, domFacade, variablesInScope, {
18181
+ currentContext: {
18182
+ formElement
18183
+ },
18184
+ functionNameResolver,
18185
+ moduleImports: {
18186
+ xf: XFORMS_NAMESPACE_URI
18187
+ },
18188
+ namespaceResolver,
18189
+ xmlSerializer: new XMLSerializer()
18190
+ });
18191
+ } catch (e) {
18192
+ formElement.dispatchEvent(new CustomEvent('error', {
18193
+ composed: false,
18194
+ bubbles: true,
18195
+ detail: {
18196
+ origin: formElement,
18197
+ message: `Expression '${xpath}' failed: ${e}`,
18198
+ expr: xpath,
18199
+ level: 'Error'
18200
+ }
18201
+ }));
18405
18202
  }
18203
+ }
18406
18204
 
18407
- /**
18408
- * @param {string} dep
18409
- * @returns {string}
18410
- */
18411
- static getBasePath(dep) {
18412
- const split = dep.split(':');
18413
- return split[0];
18205
+ /**
18206
+ * Evaluate an XPath to a set of strings
18207
+ *
18208
+ * @param {string} xpath The XPath to run
18209
+ * @param {Node} contextNode The start of the XPath
18210
+ * @param {Node} formElement The form element associated to the XPath
18211
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
18212
+ * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
18213
+ * access. This is used to determine dependencies between bind elements.
18214
+ * @return {string[]}
18215
+ */
18216
+ function evaluateXPathToStrings(xpath, contextNode, formElement, domFacade = null) {
18217
+ try {
18218
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
18219
+ return evaluateXPathToStrings$1(xpath, contextNode, domFacade, {}, {
18220
+ currentContext: {
18221
+ formElement
18222
+ },
18223
+ functionNameResolver,
18224
+ moduleImports: {
18225
+ xf: XFORMS_NAMESPACE_URI
18226
+ },
18227
+ namespaceResolver,
18228
+ xmlSerializer: new XMLSerializer()
18229
+ });
18230
+ } catch (e) {
18231
+ formElement.dispatchEvent(new CustomEvent('error', {
18232
+ composed: false,
18233
+ bubbles: true,
18234
+ detail: {
18235
+ origin: formElement,
18236
+ message: `Expression '${xpath}' failed: ${e}`,
18237
+ expr: xpath,
18238
+ level: 'Error'
18239
+ }
18240
+ }));
18414
18241
  }
18415
18242
  }
18416
18243
 
18417
18244
  /**
18418
- * A simple dependency graph
18419
- *
18420
- * based on the work of https://github.com/jriecken/dependency-graph but working on ES6.
18421
- *
18422
- * Furthermore instead of the DepGraphCycleError a compute-exception event is dispatched.
18423
- *
18245
+ * Evaluate an XPath to a number
18424
18246
  *
18247
+ * @param {string} xpath The XPath to run
18248
+ * @param {Node} contextNode The start of the XPath
18249
+ * @param {Node} formElement The form element associated to the XPath
18250
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
18251
+ * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
18252
+ * access. This is used to determine dependencies between bind elements.
18253
+ * @return {number}
18425
18254
  */
18426
-
18427
- /**
18428
- * Cycle error, including the path of the cycle.
18429
- */
18430
- // const DepGraphCycleError = (exports.DepGraphCycleError = function (cyclePath) {
18431
-
18432
- /*
18433
- export function DepGraphCycleError(cyclePath) {
18434
- const message = "Dependency Cycle Found: " + cyclePath.join(" -> ");
18435
- const instance = new Error(message);
18436
- instance.cyclePath = cyclePath;
18437
- Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
18438
- if (Error.captureStackTrace) {
18439
- Error.captureStackTrace(instance, DepGraphCycleError);
18255
+ function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade = null) {
18256
+ try {
18257
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
18258
+ const variablesInScope = getVariablesInScope(formElement);
18259
+ return evaluateXPathToNumber$1(xpath, contextNode, domFacade, variablesInScope, {
18260
+ currentContext: {
18261
+ formElement
18262
+ },
18263
+ functionNameResolver,
18264
+ moduleImports: {
18265
+ xf: XFORMS_NAMESPACE_URI
18266
+ },
18267
+ namespaceResolver,
18268
+ xmlSerializer: new XMLSerializer()
18269
+ });
18270
+ } catch (e) {
18271
+ formElement.dispatchEvent(new CustomEvent('error', {
18272
+ composed: false,
18273
+ bubbles: true,
18274
+ detail: {
18275
+ origin: formElement,
18276
+ message: `Expression '${xpath}' failed: ${e}`,
18277
+ expr: xpath,
18278
+ level: 'Error'
18279
+ }
18280
+ }));
18440
18281
  }
18441
- return instance;
18442
- };
18443
-
18444
- DepGraphCycleError.prototype = Object.create(Error.prototype, {
18445
- constructor: {
18446
- value: Error,
18447
- enumerable: false,
18448
- writable: true,
18449
- configurable: true
18282
+ }
18283
+ const contextFunction = (dynamicContext, string) => {
18284
+ const caller = dynamicContext.currentContext.formElement;
18285
+ let instance = null;
18286
+ if (string) {
18287
+ instance = resolveId(string, caller);
18288
+ } else {
18289
+ instance = XPathUtil.getParentBindingElement(caller);
18450
18290
  }
18451
- });
18452
- Object.setPrototypeOf(DepGraphCycleError, Error);
18453
- */
18454
-
18455
- /**
18456
- * Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
18457
- *
18458
- * Detects cycles and throws an Error if one is detected (unless the "circular"
18459
- * parameter is "true" in which case it ignores them).
18460
- *
18461
- * @param edges The set of edges to DFS through
18462
- * @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
18463
- * @param result An array in which the results will be populated
18464
- * @param circular A boolean to allow circular dependencies
18465
- */
18466
- function createDFS(edges, leavesOnly, result, circular) {
18467
- const visited = {};
18468
- // eslint-disable-next-line func-names
18469
- return function (start) {
18470
- // console.log('start ', start);
18471
- if (visited[start]) {
18472
- return;
18473
- }
18474
- const inCurrentPath = {};
18475
- const currentPath = [];
18476
- const todo = []; // used as a stack
18477
- todo.push({
18478
- node: start,
18479
- processed: false
18480
- });
18481
- while (todo.length > 0) {
18482
- const current = todo[todo.length - 1]; // peek at the todo stack
18483
- const {
18484
- processed
18485
- } = current;
18291
+ if (instance) {
18292
+ if (instance.nodeName === 'FX-REPEAT') {
18486
18293
  const {
18487
- node
18488
- } = current;
18489
- if (!processed) {
18490
- // Haven't visited edges yet (visiting phase)
18491
- if (visited[node]) {
18492
- todo.pop();
18493
- // eslint-disable-next-line no-continue
18494
- continue;
18495
- } else if (inCurrentPath[node]) {
18496
- // It's not a DAG
18497
- if (circular) {
18498
- todo.pop();
18499
- // If we're tolerating cycles, don't revisit the node
18500
- // eslint-disable-next-line no-continue
18501
- continue;
18502
- }
18503
- currentPath.push(node);
18504
- window.dispatchEvent(new CustomEvent('compute-exception', {
18505
- composed: false,
18506
- bubbles: true,
18507
- detail: {
18508
- path: currentPath,
18509
- message: 'cyclic graph'
18510
- }
18511
- }));
18512
- // return;
18513
- // console.log('‘circular path: ' + currentPath);
18514
- // throw new DepGraphCycleError(currentPath);
18515
-
18516
- // Stop all processing. This form is broken and we should not break the browser
18517
- throw new Error(`Cyclic at ${currentPath}`);
18518
- }
18519
- inCurrentPath[node] = true;
18520
- currentPath.push(node);
18521
- const nodeEdges = edges[node];
18522
- // (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation)
18523
- for (let i = nodeEdges.length - 1; i >= 0; i -= 1) {
18524
- todo.push({
18525
- node: nodeEdges[i],
18526
- processed: false
18527
- });
18528
- }
18529
- current.processed = true;
18530
- } else {
18531
- // Have visited edges (stack unrolling phase)
18532
- todo.pop();
18533
- currentPath.pop();
18534
- inCurrentPath[node] = false;
18535
- visited[node] = true;
18536
- if (!leavesOnly || edges[node].length === 0) {
18537
- result.push(node);
18294
+ nodeset
18295
+ } = instance;
18296
+ for (let parent = caller; parent; parent = parent.parentNode) {
18297
+ if (parent.parentNode === instance) {
18298
+ const offset = Array.from(parent.parentNode.children).indexOf(parent);
18299
+ return nodeset[offset];
18538
18300
  }
18539
18301
  }
18540
18302
  }
18541
- };
18542
- }
18303
+ return instance.nodeset;
18304
+ }
18305
+ return caller.getInScopeContext();
18306
+ };
18307
+
18308
+ // todo: implement
18309
+ const currentFunction = (dynamicContext, string) => {
18310
+ dynamicContext.currentContext.formElement;
18311
+ return null;
18312
+ };
18313
+ const elementFunction = (dynamicContext, string) => {
18314
+ dynamicContext.currentContext.formElement;
18315
+ const newElement = document.createElement(string);
18316
+ return newElement;
18317
+ };
18543
18318
 
18544
18319
  /**
18545
- * Simple Dependency Graph
18320
+ * @param id as string
18321
+ * @return instance data for given id serialized to string.
18546
18322
  */
18323
+ registerCustomXPathFunction({
18324
+ namespaceURI: XFORMS_NAMESPACE_URI,
18325
+ localName: 'context'
18326
+ }, [], 'item()?', contextFunction);
18547
18327
 
18548
- /*
18549
- var DepGraph = (exports.DepGraph = function DepGraph(opts) {
18550
- this.nodes = {}; // Node -> Node/Data (treated like a Set)
18551
- this.outgoingEdges = {}; // Node -> [Dependency Node]
18552
- this.incomingEdges = {}; // Node -> [Dependant Node]
18553
- this.circular = opts && !!opts.circular; // Allows circular deps
18554
- });
18555
- */
18328
+ /**
18329
+ * @param id as string
18330
+ * @return instance data for given id serialized to string.
18331
+ */
18332
+ registerCustomXPathFunction({
18333
+ namespaceURI: XFORMS_NAMESPACE_URI,
18334
+ localName: 'context'
18335
+ }, ['xs:string'], 'item()?', contextFunction);
18336
+ registerCustomXPathFunction({
18337
+ namespaceURI: XFORMS_NAMESPACE_URI,
18338
+ localName: 'current'
18339
+ }, ['xs:string'], 'item()?', currentFunction);
18340
+ registerCustomXPathFunction({
18341
+ namespaceURI: XFORMS_NAMESPACE_URI,
18342
+ localName: 'element'
18343
+ }, ['xs:string'], 'item()?', elementFunction);
18556
18344
 
18557
- function DepGraph(opts) {
18558
- this.nodes = {}; // Node -> Node/Data (treated like a Set)
18559
- this.outgoingEdges = {}; // Node -> [Dependency Node]
18560
- this.incomingEdges = {}; // Node -> [Dependant Node]
18561
- this.circular = opts && !!opts.circular; // Allows circular deps
18562
- }
18345
+ /**
18346
+ * @param id as string
18347
+ * @return instance data for given id serialized to string.
18348
+ */
18349
+ registerCustomXPathFunction({
18350
+ namespaceURI: XFORMS_NAMESPACE_URI,
18351
+ localName: 'log'
18352
+ }, ['xs:string?'], 'xs:string?', (dynamicContext, string) => {
18353
+ const {
18354
+ formElement
18355
+ } = dynamicContext.currentContext;
18356
+ const instance = resolveId(string, formElement, 'fx-instance');
18357
+ if (instance) {
18358
+ if (instance.getAttribute('type') === 'json') {
18359
+ console.warn('log() does not work for JSON yet');
18360
+ // return JSON.stringify(instance.getDefaultContext());
18361
+ } else {
18362
+ const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
18363
+ return prettifyXml(def);
18364
+ }
18365
+ }
18366
+ return null;
18367
+ });
18368
+ registerCustomXPathFunction({
18369
+ namespaceURI: XFORMS_NAMESPACE_URI,
18370
+ localName: 'fore-attr'
18371
+ }, ['xs:string?'], 'xs:string?', (dynamicContext, string) => {
18372
+ const {
18373
+ formElement
18374
+ } = dynamicContext.currentContext;
18375
+ let parent = formElement;
18376
+ if (formElement.nodeType === Node.TEXT_NODE) {
18377
+ parent = formElement.parentNode;
18378
+ }
18379
+ const foreElement = parent.closest('fx-fore');
18380
+ if (foreElement.hasAttribute(string)) {
18381
+ return foreElement.getAttribute(string);
18382
+ }
18383
+ return null;
18384
+ });
18385
+ registerCustomXPathFunction({
18386
+ namespaceURI: XFORMS_NAMESPACE_URI,
18387
+ localName: 'parse'
18388
+ }, ['xs:string?'], 'element()?', (_dynamicContext, string) => {
18389
+ const parser = new DOMParser();
18390
+ const out = parser.parseFromString(string, 'application/xml');
18391
+ console.log('parse', out);
18563
18392
 
18564
- DepGraph.prototype = {
18565
- /**
18566
- * The number of nodes in the graph.
18567
- */
18568
- size() {
18569
- return Object.keys(this.nodes).length;
18570
- },
18571
- /**
18572
- * Add a node to the dependency graph. If a node already exists, this method will do nothing.
18573
- */
18574
- addNode(node, data) {
18575
- if (!this.hasNode(node)) {
18576
- // Checking the arguments length allows the user to add a node with undefined data
18577
- if (arguments.length === 2) {
18578
- this.nodes[node] = data;
18393
+ /*
18394
+ const {formElement} = dynamicContext.currentContext;
18395
+ const instance = resolveId(string, formElement, 'fx-instance');
18396
+ if (instance) {
18397
+ if (instance.getAttribute('type') === 'json') {
18398
+ console.warn('log() does not work for JSON yet');
18399
+ // return JSON.stringify(instance.getDefaultContext());
18400
+ } else {
18401
+ const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
18402
+ return Fore.prettifyXml(def);
18403
+ }
18404
+ }
18405
+ */
18406
+ return out.firstElementChild;
18407
+ });
18408
+ function buildTree(tree, data) {
18409
+ if (!data) return;
18410
+ if (data.nodeType === Node.ELEMENT_NODE) {
18411
+ if (data.children) {
18412
+ const details = document.createElement('details');
18413
+ details.setAttribute('data-path', data.nodeName);
18414
+ const summary = document.createElement('summary');
18415
+ let display = ` <${data.nodeName}`;
18416
+ Array.from(data.attributes).forEach(attr => {
18417
+ display += ` ${attr.nodeName}="${attr.nodeValue}"`;
18418
+ });
18419
+ let contents;
18420
+ if (data.firstChild && data.firstChild.nodeType === Node.TEXT_NODE && data.firstChild.data.trim() !== '') {
18421
+ // console.log('whoooooooooopp');
18422
+ contents = data.firstChild.nodeValue;
18423
+ display += `>${contents}</${data.nodeName}>`;
18579
18424
  } else {
18580
- this.nodes[node] = node;
18425
+ display += '>';
18581
18426
  }
18582
- this.outgoingEdges[node] = [];
18583
- this.incomingEdges[node] = [];
18584
- }
18585
- },
18586
- /**
18587
- * Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
18588
- */
18589
- removeNode(node) {
18590
- if (this.hasNode(node)) {
18591
- delete this.nodes[node];
18592
- delete this.outgoingEdges[node];
18593
- delete this.incomingEdges[node];
18594
- // [this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) {
18595
- [this.incomingEdges, this.outgoingEdges].forEach(edgeList => {
18596
- Object.keys(edgeList).forEach(key => {
18597
- const idx = edgeList[key].indexOf(node);
18598
- if (idx >= 0) {
18599
- edgeList[key].splice(idx, 1);
18600
- }
18601
- }, this);
18427
+ summary.textContent = display;
18428
+ details.appendChild(summary);
18429
+ if (data.childElementCount !== 0) {
18430
+ details.setAttribute('open', 'open');
18431
+ } else {
18432
+ summary.setAttribute('style', 'list-style:none;');
18433
+ }
18434
+ tree.appendChild(details);
18435
+ Array.from(data.children).forEach(child => {
18436
+ // if(child.nodeType === Node.ELEMENT_NODE){
18437
+ // child.parentNode.appendChild(buildTree(child));
18438
+ buildTree(details, child);
18439
+ // }
18602
18440
  });
18603
18441
  }
18604
- },
18605
- /**
18606
- * Check if a node exists in the graph
18607
- */
18608
- hasNode(node) {
18609
- // return this.nodes.hasOwnProperty(node);
18442
+ } /* else if(data.nodeType === Node.ATTRIBUTE_NODE){
18443
+ //create span for now
18444
+ // const span = document.createElement('span');
18445
+ // span.style.background = 'grey';
18446
+ // span.textContent = data.value;
18447
+ // tree.appendChild(span);
18448
+ tree.setAttribute(data.nodeName,data.value);
18449
+ }else {
18450
+ tree.textContent = data;
18451
+ } */
18610
18452
 
18611
- return Object.prototype.hasOwnProperty.call(this.nodes, node);
18612
- },
18613
- /**
18614
- * Get the data associated with a node name
18615
- */
18616
- getNodeData(node) {
18617
- if (this.hasNode(node)) {
18618
- return this.nodes[node];
18453
+ // return tree;
18454
+ }
18455
+
18456
+ registerCustomXPathFunction({
18457
+ namespaceURI: XFORMS_NAMESPACE_URI,
18458
+ localName: 'logtree'
18459
+ }, ['xs:string?'], 'element()?', (dynamicContext, string) => {
18460
+ const {
18461
+ formElement
18462
+ } = dynamicContext.currentContext;
18463
+ const instance = resolveId(string, formElement, 'fx-instance');
18464
+ if (instance) {
18465
+ // const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
18466
+ // const def = JSON.stringify(instance.getDefaultContext());
18467
+
18468
+ const treeDiv = document.createElement('div');
18469
+ treeDiv.setAttribute('class', 'logtree');
18470
+ // const datatree = buildTree(tree,instance.getDefaultContext());
18471
+ // return tree.appendChild(datatree);
18472
+ // return buildTree(root,instance.getDefaultContext());;
18473
+ const form = dynamicContext.currentContext.formElement;
18474
+ const logtree = form.querySelector('.logtree');
18475
+ if (logtree) {
18476
+ logtree.parentNode.removeChild(logtree);
18619
18477
  }
18620
- throw new Error(`Node does not exist: ${node}`);
18621
- },
18622
- /**
18623
- * Set the associated data for a given node name. If the node does not exist, this method will throw an error
18624
- */
18625
- setNodeData(node, data) {
18626
- if (this.hasNode(node)) {
18627
- this.nodes[node] = data;
18628
- } else {
18629
- throw new Error(`Node does not exist: ${node}`);
18478
+ const tree = buildTree(treeDiv, instance.getDefaultContext());
18479
+ if (tree) {
18480
+ form.appendChild(tree);
18630
18481
  }
18631
- },
18482
+ }
18483
+ return null;
18484
+ });
18485
+ const instance = (dynamicContext, string) => {
18486
+ // Spec: https://www.w3.org/TR/xforms-xpath/#The_XForms_Function_Library#The_instance.28.29_Function
18487
+ // TODO: handle no string passed (null will be passed instead)
18488
+
18632
18489
  /**
18633
- * Add a dependency between two nodes. If either of the nodes does not exist,
18634
- * an Error will be thrown.
18490
+ * @type {import('./fx-fore.js').FxFore}
18635
18491
  */
18636
- addDependency(from, to) {
18637
- if (!this.hasNode(from)) {
18638
- throw new Error(`Node does not exist: ${from}`);
18639
- }
18640
- if (!this.hasNode(to)) {
18641
- throw new Error(`Node does not exist: ${to}`);
18642
- }
18643
- if (this.outgoingEdges[from].indexOf(to) === -1) {
18644
- this.outgoingEdges[from].push(to);
18492
+ const formElement = evaluateXPathToFirstNode$1('ancestor-or-self::fx-fore[1]', dynamicContext.currentContext.formElement, null, null, {
18493
+ namespaceResolver: xhtmlNamespaceResolver
18494
+ });
18495
+ let lookup = null;
18496
+ if (string === null || string === 'default') {
18497
+ lookup = formElement.getModel().getDefaultInstance();
18498
+ } else {
18499
+ lookup = formElement.getModel().getInstance(string);
18500
+ if (!lookup) {
18501
+ document.querySelector('fx-fore').dispatchEvent(new CustomEvent('error', {
18502
+ composed: true,
18503
+ bubbles: true,
18504
+ detail: {
18505
+ origin: 'functions',
18506
+ message: `Instance not found '${string}'`,
18507
+ level: 'Error'
18508
+ }
18509
+ }));
18645
18510
  }
18646
- if (this.incomingEdges[to].indexOf(from) === -1) {
18647
- this.incomingEdges[to].push(from);
18511
+ }
18512
+ const context = lookup.getDefaultContext();
18513
+ if (!context) {
18514
+ return null;
18515
+ }
18516
+ return context;
18517
+ };
18518
+ registerCustomXPathFunction({
18519
+ namespaceURI: XFORMS_NAMESPACE_URI,
18520
+ localName: 'index'
18521
+ }, ['xs:string?'], 'xs:integer?', (dynamicContext, string) => {
18522
+ const {
18523
+ formElement
18524
+ } = dynamicContext.currentContext;
18525
+ if (string === null) {
18526
+ return 1;
18527
+ }
18528
+ const repeat = resolveId(string, formElement, 'fx-repeat');
18529
+
18530
+ // const def = instance.getInstanceData();
18531
+ if (repeat) {
18532
+ return repeat.getAttribute('index');
18533
+ }
18534
+ return Number(1);
18535
+ });
18536
+
18537
+ // Note that this is not to spec. The spec enforces elements to be returned from the
18538
+ // instance. However, we allow instances to actually be JSON!
18539
+ registerCustomXPathFunction({
18540
+ namespaceURI: XFORMS_NAMESPACE_URI,
18541
+ localName: 'instance'
18542
+ }, [], 'item()?', domFacade => instance(domFacade, null));
18543
+ registerCustomXPathFunction({
18544
+ namespaceURI: XFORMS_NAMESPACE_URI,
18545
+ localName: 'instance'
18546
+ }, ['xs:string?'], 'item()?', instance);
18547
+ const jsonToXml = (_dynamicContext, json) => {
18548
+ const escapeXml = str => str.replace(/[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
18549
+ const convert = (obj, parent) => {
18550
+ const type = typeof obj;
18551
+ if (type === 'number') {
18552
+ parent.setAttribute('type', 'number');
18553
+ parent.textContent = obj.toString();
18554
+ } else if (type === 'boolean') {
18555
+ parent.setAttribute('type', 'boolean');
18556
+ parent.textContent = obj.toString();
18557
+ } else if (obj === null) {
18558
+ const node = document.createElement('_');
18559
+ node.setAttribute('type', 'null');
18560
+ parent.appendChild(node);
18561
+ } else if (type === 'string') {
18562
+ parent.textContent = escapeXml(obj);
18563
+ } else if (Array.isArray(obj)) {
18564
+ parent.setAttribute('type', 'array');
18565
+ obj.forEach(item => {
18566
+ const node = document.createElement('_');
18567
+ convert(item, node);
18568
+ node.textContent = item;
18569
+ parent.appendChild(node);
18570
+ });
18571
+ } else if (type === 'object') {
18572
+ parent.setAttribute('type', 'object');
18573
+ Object.entries(obj).forEach(([key, value]) => {
18574
+ if (value) {
18575
+ const childNode = document.createElement(key.replace(/[^a-zA-Z0-9_]/g, '_'));
18576
+ convert(value, childNode);
18577
+ parent.appendChild(childNode);
18578
+ }
18579
+ });
18648
18580
  }
18649
- return true;
18650
- },
18651
- /**
18652
- * Remove a dependency between two nodes.
18653
- */
18654
- removeDependency(from, to) {
18655
- let idx;
18656
- if (this.hasNode(from)) {
18657
- idx = this.outgoingEdges[from].indexOf(to);
18658
- if (idx >= 0) {
18659
- this.outgoingEdges[from].splice(idx, 1);
18581
+ };
18582
+ const root = document.createElement('json');
18583
+ if (Array.isArray(json)) {
18584
+ root.setAttribute('type', 'array');
18585
+ } else {
18586
+ root.setAttribute('type', 'object');
18587
+ }
18588
+ convert(json, root);
18589
+ // return root.outerHTML;
18590
+ console.log('xml', root);
18591
+ return root;
18592
+ };
18593
+ registerCustomXPathFunction({
18594
+ namespaceURI: XFORMS_NAMESPACE_URI,
18595
+ localName: 'json2xml'
18596
+ }, ['item()?'], 'item()?', jsonToXml);
18597
+ const xmlToJson = (_dynamicContext, xml) => {
18598
+ const isElementNode = node => node.nodeType === Node.ELEMENT_NODE;
18599
+ const isTextNode = node => node.nodeType === Node.TEXT_NODE;
18600
+ const parseNode = node => {
18601
+ if (isElementNode(node)) {
18602
+ const obj = {};
18603
+ if (node.hasAttributes()) {
18604
+ obj.type = node.getAttribute('type');
18660
18605
  }
18661
- }
18662
- if (this.hasNode(to)) {
18663
- idx = this.incomingEdges[to].indexOf(from);
18664
- if (idx >= 0) {
18665
- this.incomingEdges[to].splice(idx, 1);
18606
+ if (node.childNodes.length === 1 && isTextNode(node.firstChild)) {
18607
+ return node.textContent;
18666
18608
  }
18667
- }
18668
- },
18669
- /**
18670
- * Return a clone of the dependency graph. If any custom data is attached
18671
- * to the nodes, it will only be shallow copied.
18672
- */
18673
- clone() {
18674
- const source = this;
18675
- const result = new DepGraph();
18676
- const keys = Object.keys(source.nodes);
18677
- keys.forEach(n => {
18678
- result.nodes[n] = source.nodes[n];
18679
- result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
18680
- result.incomingEdges[n] = source.incomingEdges[n].slice(0);
18681
- });
18682
- return result;
18683
- },
18684
- /**
18685
- * Get an array containing the direct dependencies of the specified node.
18686
- *
18687
- * Throws an Error if the specified node does not exist.
18688
- */
18689
- directDependenciesOf(node) {
18690
- if (this.hasNode(node)) {
18691
- return this.outgoingEdges[node].slice(0);
18692
- }
18693
- throw new Error(`Node does not exist: ${node}`);
18694
- },
18695
- /**
18696
- * Get an array containing the nodes that directly depend on the specified node.
18697
- *
18698
- * Throws an Error if the specified node does not exist.
18699
- */
18700
- directDependantsOf(node) {
18701
- if (this.hasNode(node)) {
18702
- return this.incomingEdges[node].slice(0);
18703
- }
18704
- throw new Error(`Node does not exist: ${node}`);
18705
- },
18706
- /**
18707
- * Get an array containing the nodes that the specified node depends on (transitively).
18708
- *
18709
- * Throws an Error if the graph has a cycle, or the specified node does not exist.
18710
- *
18711
- * If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
18712
- * in the array.
18713
- */
18714
- dependenciesOf(node, leavesOnly) {
18715
- if (this.hasNode(node)) {
18716
- const result = [];
18717
- const DFS = createDFS(this.outgoingEdges, leavesOnly, result, this.circular);
18718
- DFS(node);
18719
- const idx = result.indexOf(node);
18720
- if (idx >= 0) {
18721
- result.splice(idx, 1);
18609
+ for (const child of node.childNodes) {
18610
+ const childName = child.nodeName;
18611
+ const childValue = parseNode(child);
18612
+ if (obj[childName]) {
18613
+ if (!Array.isArray(obj[childName])) {
18614
+ obj[childName] = [obj[childName]];
18615
+ }
18616
+ obj[childName].push(childValue);
18617
+ } else {
18618
+ obj[childName] = childValue;
18619
+ }
18722
18620
  }
18723
- return result;
18621
+ return obj;
18724
18622
  }
18725
- throw new Error(`Node does not exist: ${node}`);
18726
- },
18727
- /**
18728
- * get an array containing the nodes that depend on the specified node (transitively).
18729
- *
18730
- * Throws an Error if the graph has a cycle, or the specified node does not exist.
18731
- *
18732
- * If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
18733
- */
18734
- dependantsOf(node, leavesOnly) {
18735
- if (this.hasNode(node)) {
18736
- const result = [];
18737
- const DFS = createDFS(this.incomingEdges, leavesOnly, result, this.circular);
18738
- DFS(node);
18739
- const idx = result.indexOf(node);
18740
- if (idx >= 0) {
18741
- result.splice(idx, 1);
18742
- }
18743
- return result;
18623
+ if (isTextNode(node)) {
18624
+ return node.textContent;
18744
18625
  }
18745
- throw new Error(`Node does not exist: ${node}`);
18746
- },
18747
- /**
18748
- * Get an array of nodes that have no dependants (i.e. nothing depends on them).
18749
- */
18750
- entryNodes() {
18751
- const self = this;
18752
- return Object.keys(this.nodes).filter(node => self.incomingEdges[node].length === 0);
18753
- },
18754
- /**
18755
- * Construct the overall processing order for the dependency graph.
18756
- *
18757
- * Throws an Error if the graph has a cycle.
18758
- *
18759
- * If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
18760
- */
18761
- overallOrder(leavesOnly) {
18762
- const self = this;
18763
- const result = [];
18764
- const keys = Object.keys(this.nodes);
18765
- if (keys.length === 0) {
18766
- return result; // Empty graph
18626
+ return undefined;
18627
+ };
18628
+ const parser = new DOMParser();
18629
+ const xmlDoc = parser.parseFromString(xml, 'application/xml');
18630
+ const root = xmlDoc.documentElement;
18631
+ return parseNode(root);
18632
+ };
18633
+ registerCustomXPathFunction({
18634
+ namespaceURI: XFORMS_NAMESPACE_URI,
18635
+ localName: 'xmltoJson'
18636
+ }, ['item()?'], 'item()?', xmlToJson);
18637
+
18638
+ /*
18639
+ // Example usage:
18640
+ const xml = '<json type="object"><given>Mark</given><family>Smith</family></json>';
18641
+ console.log(xmlToJson(xml));
18642
+ */
18643
+
18644
+ registerCustomXPathFunction({
18645
+ namespaceURI: XFORMS_NAMESPACE_URI,
18646
+ localName: 'depends'
18647
+ }, ['node()*'], 'item()?', (_dynamicContext, nodes) =>
18648
+ // console.log('depends on : ', nodes[0]);
18649
+ nodes[0]);
18650
+ registerCustomXPathFunction({
18651
+ namespaceURI: XFORMS_NAMESPACE_URI,
18652
+ localName: 'event'
18653
+ }, ['xs:string?'], 'item()?', (dynamicContext, arg) => {
18654
+ if (!arg) return null;
18655
+ for (let ancestor = dynamicContext.currentContext.formElement; ancestor; ancestor = ancestor.parentNode) {
18656
+ if (!ancestor.currentEvent) {
18657
+ continue;
18767
18658
  }
18768
18659
 
18769
- if (!this.circular) {
18770
- // Look for cycles - we run the DFS starting at all the nodes in case there
18771
- // are several disconnected subgraphs inside this dependency graph.
18772
- const CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
18773
- keys.forEach(n => {
18774
- CycleDFS(n);
18775
- });
18660
+ // We have a current event. read the property either from detail, or from the event
18661
+ // itself.
18662
+ // Check detail for custom events! This is how that is passed along
18663
+ if (ancestor.currentEvent.detail && typeof ancestor.currentEvent.detail === 'object' && arg in ancestor.currentEvent.detail) {
18664
+ return ancestor.currentEvent.detail[arg];
18776
18665
  }
18777
- const DFS = createDFS(this.outgoingEdges, leavesOnly, result, this.circular);
18778
- // Find all potential starting points (nodes with nothing depending on them) an
18779
- // run a DFS starting at these points to get the order
18780
- keys.filter(node => self.incomingEdges[node].length === 0).forEach(n => {
18781
- DFS(n);
18782
- });
18783
18666
 
18784
- // If we're allowing cycles - we need to run the DFS against any remaining
18785
- // nodes that did not end up in the initial result (as they are part of a
18786
- // subgraph that does not have a clear starting point)
18787
- if (this.circular) {
18788
- keys.filter(node => result.indexOf(node) === -1).forEach(n => DFS(n));
18667
+ // arg might be `code`, so currentEvent.code should work
18668
+ if (arg.includes('.')) {
18669
+ return _propertyLookup(ancestor.currentEvent, arg);
18789
18670
  }
18790
- return result;
18671
+ return ancestor.currentEvent[arg] || null;
18672
+ }
18673
+ return null;
18674
+ });
18675
+ function _propertyLookup(obj, path) {
18676
+ const parts = path.split('.');
18677
+ if (parts.length == 1) {
18678
+ return obj[parts[0]];
18791
18679
  }
18680
+ return _propertyLookup(obj[parts[0]], parts.slice(1).join('.'));
18681
+ }
18682
+
18683
+ // Implement the XForms standard functions here.
18684
+ registerXQueryModule(`
18685
+ module namespace xf="${XFORMS_NAMESPACE_URI}";
18686
+
18687
+ declare %public function xf:boolean-from-string($str as xs:string) as xs:boolean {
18688
+ lower-case($str) = "true" or $str = "1"
18689
+ };
18690
+ `);
18691
+
18692
+ // How to run XQUERY:
18693
+ /**
18694
+ registerXQueryModule(`
18695
+ module namespace my-custom-namespace = "my-custom-uri";
18696
+ (:~
18697
+ Insert attribute somewhere
18698
+ ~:)
18699
+ declare %public %updating function my-custom-namespace:do-something ($ele as element()) as xs:boolean {
18700
+ if ($ele/@done) then false() else
18701
+ (insert node
18702
+ attribute done {"true"}
18703
+ into $ele, true())
18792
18704
  };
18705
+ `)
18706
+ // At some point:
18707
+ const contextNode = null;
18708
+ const pendingUpdatesAndXdmValue = evaluateUpdatingExpressionSync('ns:do-something(.)', contextNode, null, null, {moduleImports: {'ns': 'my-custom-uri'}})
18793
18709
 
18794
- // Create some aliases
18795
- DepGraph.prototype.directDependentsOf = DepGraph.prototype.directDependantsOf;
18796
- DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
18710
+ console.log(pendingUpdatesAndXdmValue.xdmValue); // this is true or false, see function
18711
+
18712
+ executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null, null);
18713
+ */
18714
+
18715
+ /**
18716
+ * @param input as string
18717
+ * @return {string}
18718
+ */
18719
+ registerCustomXPathFunction({
18720
+ namespaceURI: XFORMS_NAMESPACE_URI,
18721
+ localName: 'base64encode'
18722
+ }, ['xs:string?'], 'xs:string?', (_dynamicContext, string) => btoa(string));
18723
+ registerCustomXPathFunction({
18724
+ namespaceURI: XFORMS_NAMESPACE_URI,
18725
+ localName: 'local-date'
18726
+ }, [], 'xs:string?', (_dynamicContext, _string) => new Date().toLocaleDateString());
18727
+ registerCustomXPathFunction({
18728
+ namespaceURI: XFORMS_NAMESPACE_URI,
18729
+ localName: 'local-dateTime'
18730
+ }, [], 'xs:string?', (_dynamicContext, _string) => new Date().toLocaleString());
18731
+ registerCustomXPathFunction({
18732
+ namespaceURI: XFORMS_NAMESPACE_URI,
18733
+ localName: 'uri'
18734
+ }, [], 'xs:string?', (_dynamicContext, _string) => window.location.href);
18735
+ registerCustomXPathFunction({
18736
+ namespaceURI: XFORMS_NAMESPACE_URI,
18737
+ localName: 'uri-fragment'
18738
+ }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.hash);
18739
+ registerCustomXPathFunction({
18740
+ namespaceURI: XFORMS_NAMESPACE_URI,
18741
+ localName: 'uri-host'
18742
+ }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.host);
18743
+ registerCustomXPathFunction({
18744
+ namespaceURI: XFORMS_NAMESPACE_URI,
18745
+ localName: 'uri-query'
18746
+ }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.search);
18747
+ registerCustomXPathFunction({
18748
+ namespaceURI: XFORMS_NAMESPACE_URI,
18749
+ localName: 'uri-relpath'
18750
+ }, [], 'xs:string?', (_dynamicContext, _arg) => {
18751
+ const path = new URL(window.location.href).pathname;
18752
+ return path.substring(0, path.lastIndexOf('/') + 1);
18753
+ });
18754
+ registerCustomXPathFunction({
18755
+ namespaceURI: XFORMS_NAMESPACE_URI,
18756
+ localName: 'uri-path'
18757
+ }, [], 'xs:string?', (_dynamicContext, _arg) => new URL(window.location.href).pathname);
18758
+ registerCustomXPathFunction({
18759
+ namespaceURI: XFORMS_NAMESPACE_URI,
18760
+ localName: 'uri-port'
18761
+ }, [], 'xs:string?', (_dynamicContext, _arg) => window.location.port);
18762
+ registerCustomXPathFunction({
18763
+ namespaceURI: XFORMS_NAMESPACE_URI,
18764
+ localName: 'uri-param'
18765
+ }, ['xs:string?'], 'xs:string?', (_dynamicContext, arg) => {
18766
+ if (!arg) return null;
18767
+ const {
18768
+ search
18769
+ } = window.location;
18770
+ const urlparams = new URLSearchParams(search);
18771
+ const param = urlparams.get(arg);
18772
+ return param || '';
18773
+ });
18774
+ registerCustomXPathFunction({
18775
+ namespaceURI: XFORMS_NAMESPACE_URI,
18776
+ localName: 'uri-scheme'
18777
+ }, [], 'xs:string?', (_dynamicContext, _arg) => new URL(window.location.href).protocol);
18778
+ registerCustomXPathFunction({
18779
+ namespaceURI: XFORMS_NAMESPACE_URI,
18780
+ localName: 'uri-scheme-specific-part'
18781
+ }, [], 'xs:string?', (_dynamicContext, _arg) => {
18782
+ const uri = window.location.href;
18783
+ return uri.substring(uri.indexOf(':') + 1, uri.length);
18784
+ });
18785
+
18786
+ /**
18787
+ * @param {Node} node
18788
+ * @returns string
18789
+ */
18790
+ /*
18791
+ export static getDocPath(node) {
18792
+ const path = fx.evaluateXPathToString('path()', node);
18793
+ // Path is like `$default/x[1]/y[1]`
18794
+ const shortened = XPathUtil.shortenPath(path);
18795
+ return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
18796
+ }
18797
+ */
18797
18798
 
18798
18799
  /**
18799
18800
  * @param {Node} node
@@ -19669,10 +19670,10 @@ class FxInstance extends HTMLElement {
19669
19670
  // Note: use the getter here: it might provide us with stubbed data if anything async is racing,
19670
19671
  // such as an @src attribute
19671
19672
  const instanceData = this.getInstanceData();
19672
- if (this.type === 'xml') {
19673
+ if (this.type === 'xml' || this.type === 'html') {
19673
19674
  return instanceData.firstElementChild;
19674
19675
  }
19675
- return instanceData;
19676
+ return this.instanceData;
19676
19677
  }
19677
19678
 
19678
19679
  /**
@@ -19902,13 +19903,13 @@ class ModelItem {
19902
19903
  set value(newVal) {
19903
19904
  if (!this.node) return;
19904
19905
  const oldVal = this.value;
19905
- if (newVal?.nodeType === Node.DOCUMENT_NODE) {
19906
+ if (newVal?.nodeType && newVal.nodeType === Node.DOCUMENT_NODE) {
19906
19907
  this.node.replaceWith(newVal.firstElementChild);
19907
- this.node = newVal.firstElementChild;
19908
- } else if (newVal?.nodeType === Node.ELEMENT_NODE) {
19908
+ // this.node.appendChild(newVal.firstElementChild);
19909
+ } else if (newVal?.nodeType && newVal.nodeType === Node.ELEMENT_NODE) {
19909
19910
  this.node.replaceWith(newVal);
19910
- this.node = newVal;
19911
- } else if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
19911
+ // this.node.appendChild(newVal);
19912
+ } else if (newVal?.nodeType && this.node.nodeType === Node.ATTRIBUTE_NODE) {
19912
19913
  this.node.nodeValue = newVal;
19913
19914
  } else {
19914
19915
  this.node.textContent = newVal;
@@ -21210,7 +21211,7 @@ class FxBind extends ForeElementMixin {
21210
21211
  }
21211
21212
 
21212
21213
  // ✅ only the repeat item gets the _<opNum> suffix; children do not.
21213
- const basePath = XPathUtil.getPath(node, instanceId);
21214
+ const basePath = getPath(node, instanceId);
21214
21215
  const path = opNum ? `${basePath}_${opNum}` : basePath;
21215
21216
 
21216
21217
  // const path = XPathUtil.getPath(node, instanceId);
@@ -23722,6 +23723,24 @@ const dirtyStates = {
23722
23723
  * @ts-check
23723
23724
  */
23724
23725
  class FxFore extends HTMLElement {
23726
+ // Records init gate events that have already happened for a given target (document/window/element).
23727
+ // This prevents “missed gate” situations when an fx-fore is replaced (e.g. via src loading)
23728
+ // after the init event already fired.
23729
+ static _hasSeenInitEvent(target, eventName) {
23730
+ const set = FxFore._initEventState.get(target);
23731
+ return !!(set && set.has(eventName));
23732
+ }
23733
+ static _markInitEventSeen(target, eventName) {
23734
+ let set = FxFore._initEventState.get(target);
23735
+ if (!set) {
23736
+ set = new Set();
23737
+ FxFore._initEventState.set(target, set);
23738
+ }
23739
+ set.add(eventName);
23740
+ }
23741
+ static get observedAttributes() {
23742
+ return ['src', 'selector'];
23743
+ }
23725
23744
  static get properties() {
23726
23745
  return {
23727
23746
  /**
@@ -23791,14 +23810,12 @@ class FxFore extends HTMLElement {
23791
23810
  // avoid double init
23792
23811
  if (this.inited) return;
23793
23812
 
23794
- // 2) Wait for dependencies if needed
23795
- if (this.hasAttribute('wait-for')) {
23796
- try {
23797
- await this._whenDependenciesReady();
23798
- } catch (e) {
23799
- console.warn('wait-for failed', e);
23800
- return;
23801
- }
23813
+ // 2) Wait for init gates (init-on / init-on-target / wait-for)
23814
+ try {
23815
+ await this._waitForInitGates();
23816
+ } catch (e) {
23817
+ console.warn('init gating failed', e);
23818
+ return;
23802
23819
  }
23803
23820
 
23804
23821
  // 3) Bail if we got disconnected/replaced while waiting
@@ -23807,10 +23824,21 @@ class FxFore extends HTMLElement {
23807
23824
  this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
23808
23825
  }
23809
23826
 
23827
+ // 4) Safely read assigned content
23828
+ const getAssignedElements = () => {
23829
+ if (typeof slotEl.assignedElements === 'function') {
23830
+ return slotEl.assignedElements({
23831
+ flatten: true
23832
+ });
23833
+ }
23834
+ // Fallback for odd engines/polyfills
23835
+ return (slotEl.assignedNodes({
23836
+ flatten: true
23837
+ }) || []).filter(n => n.nodeType === Node.ELEMENT_NODE);
23838
+ };
23839
+
23810
23840
  // SAFE: slotEl is the actual event source, not a fresh query
23811
- const children = slotEl.assignedElements({
23812
- flatten: true
23813
- });
23841
+ const children = getAssignedElements();
23814
23842
  let modelElement = children.find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');
23815
23843
  if (!modelElement) {
23816
23844
  const generatedModel = document.createElement('fx-model');
@@ -23831,19 +23859,27 @@ class FxFore extends HTMLElement {
23831
23859
  registerVariables(child);
23832
23860
  }
23833
23861
  })(this);
23862
+
23863
+ // Ensure all function libraries are loaded/registered before model construction,
23864
+ // so binds/calculate/XPath evaluations can safely call them.
23865
+ const libs = Array.from(this.querySelectorAll('fx-functionlib'));
23866
+ await Promise.all(libs.map(l => l.readyPromise || Promise.resolve()));
23834
23867
  await modelElement.modelConstruct();
23835
23868
  this._handleModelConstructDone();
23836
23869
  }
23837
23870
  this._createRepeatsFromAttributes();
23838
23871
  this.inited = true;
23839
23872
  };
23840
- this.version = 'Version: 2.7.2 - built on December 9, 2025 15:22:48';
23873
+ this.version = 'Version: 2.9.0 - built on January 21, 2026 15:42:06';
23841
23874
 
23842
23875
  /**
23843
23876
  * @type {import('./fx-model.js').FxModel}
23844
23877
  */
23845
23878
  this.model = null;
23846
23879
  this.inited = false;
23880
+ this._initGatesPromise = null;
23881
+ this._warnedWaitForDeprecation = false;
23882
+ this._srcLoadPromise = null;
23847
23883
  // this.addEventListener('model-construct-done', this._handleModelConstructDone);
23848
23884
  // todo: refactoring - these should rather go into connectedcallback
23849
23885
  this.addEventListener('message', this._displayMessage);
@@ -23985,94 +24021,217 @@ class FxFore extends HTMLElement {
23985
24021
  }
23986
24022
 
23987
24023
  /**
23988
- * Resolve elements from the `wait-for` attribute.
23989
- * Supports comma-separated CSS selectors and the special value "closest".
24024
+ * Parse a list of target specs.
24025
+ *
24026
+ * We accept both comma- and whitespace-separated lists (for backward compatibility with `wait-for`).
24027
+ * Each token can be:
24028
+ * - "self" (default)
24029
+ * - "closest" (closest fx-fore)
24030
+ * - "document"
24031
+ * - "window"
24032
+ * - a CSS selector (no whitespace)
23990
24033
  */
23991
- _resolveDependencies() {
23992
- const raw = this.getAttribute('wait-for');
24034
+ _parseTargetList(raw) {
23993
24035
  if (!raw) return [];
23994
- const sels = raw.split(',').map(s => s.trim()).filter(Boolean);
24036
+ return raw.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
24037
+ }
24038
+ _findBySelector(sel) {
23995
24039
  const roots = [this.getRootNode?.() ?? document, document];
23996
- const out = [];
23997
- for (const sel of sels) {
23998
- let el = null;
23999
- if (sel === 'closest') {
24000
- el = this.closest('fx-fore');
24001
- } else {
24002
- for (const r of roots) {
24003
- if (r && 'querySelector' in r) {
24004
- el = r.querySelector(sel);
24005
- if (el) break;
24006
- }
24007
- }
24040
+ for (const r of roots) {
24041
+ if (r && 'querySelector' in r) {
24042
+ const el = r.querySelector(sel);
24043
+ if (el) return el;
24008
24044
  }
24009
- if (el) out.push(el);
24010
24045
  }
24011
- return out;
24046
+ return null;
24047
+ }
24048
+ _isReadyTarget(el) {
24049
+ return !!(el && (el.ready === true || el.classList && el.classList.contains('fx-ready') || typeof el.hasAttribute === 'function' && el.hasAttribute('ready')));
24012
24050
  }
24013
24051
 
24014
24052
  /**
24015
- * Wait until all dependencies are ready (i.e., they set `ready = true`
24016
- * and dispatch the `ready` event).
24053
+ * Collect all init gates derived from attributes.
24054
+ *
24055
+ * - `wait-for` (DEPRECATED) becomes: init-on="ready" + init-on-target=<list>
24056
+ * - `init-on` / `init-on-target` define a generic event gate
24017
24057
  */
24018
- _whenDependenciesReady() {
24019
- const raw = this.getAttribute('wait-for');
24020
- if (!raw) return Promise.resolve();
24021
- const sels = raw.split(',').map(s => s.trim()).filter(Boolean);
24022
- const roots = [this.getRootNode?.() ?? document, document];
24023
- const query = sel => {
24024
- for (const r of roots) {
24025
- if (r && 'querySelector' in r) {
24026
- const el = r.querySelector(sel);
24027
- if (el) return el;
24028
- }
24058
+ _collectInitGates() {
24059
+ const gates = [];
24060
+ const waitForRaw = this.getAttribute('wait-for');
24061
+ if (waitForRaw) {
24062
+ if (!this._warnedWaitForDeprecation) {
24063
+ console.warn('[fx-fore] The "wait-for" attribute is deprecated. Use init-on="ready" init-on-target="..." instead.');
24064
+ this._warnedWaitForDeprecation = true;
24065
+ }
24066
+ const deps = this._parseTargetList(waitForRaw);
24067
+ for (const dep of deps) {
24068
+ gates.push({
24069
+ event: 'ready',
24070
+ targetSpec: dep
24071
+ });
24029
24072
  }
24030
- return null;
24031
- };
24032
- const isReadyNow = sel => {
24033
- if (sel === 'closest') {
24034
- const outer = this.closest('fx-fore');
24035
- return !!(outer && outer.ready === true);
24073
+ }
24074
+ const initOn = this.getAttribute('init-on');
24075
+ const initOnTargetRaw = this.getAttribute('init-on-target');
24076
+ if (initOn || initOnTargetRaw) {
24077
+ const eventName = initOn || 'ready';
24078
+ const targets = initOnTargetRaw ? this._parseTargetList(initOnTargetRaw) : ['self'];
24079
+ for (const t of targets) {
24080
+ gates.push({
24081
+ event: eventName,
24082
+ targetSpec: t
24083
+ });
24036
24084
  }
24037
- const el = query(sel);
24038
- return !!(el && el.ready === true);
24039
- };
24040
- const waitOne = sel => new Promise(resolve => {
24041
- // fast path
24042
- if (isReadyNow(sel)) return resolve();
24085
+ }
24086
+ return gates;
24087
+ }
24088
+ _waitForEvent(target, eventName, isSatisfiedFn = null) {
24089
+ // If a caller provides an explicit satisfaction check, honor it first.
24090
+ if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
24091
+ FxFore._markInitEventSeen(target, eventName);
24092
+ return Promise.resolve();
24093
+ }
24043
24094
 
24044
- // robust path: listen at the document/root so replacement doesn't matter
24045
- const onReady = ev => {
24046
- const t = ev.target;
24047
- if (sel === 'closest') {
24048
- // outer fore becoming ready anywhere above us
24049
- if (t?.tagName === 'FX-FORE' && t.contains(this)) {
24050
- cleanup();
24051
- resolve();
24052
- }
24053
- } else if (t?.matches?.(sel)) {
24054
- cleanup();
24055
- resolve();
24056
- }
24095
+ // Sticky gate: if this event already happened on this target, don't wait again.
24096
+ if (FxFore._hasSeenInitEvent(target, eventName)) {
24097
+ return Promise.resolve();
24098
+ }
24099
+ return new Promise(resolve => {
24100
+ const ac = new AbortController();
24101
+ const on = () => {
24102
+ FxFore._markInitEventSeen(target, eventName);
24103
+ ac.abort();
24104
+ resolve();
24105
+ };
24106
+ target.addEventListener(eventName, on, {
24107
+ once: true,
24108
+ signal: ac.signal
24109
+ });
24110
+ });
24111
+ }
24112
+ _waitForMatchingEvent(eventName, matchesEventFn, recheckFn = null) {
24113
+ if (typeof recheckFn === 'function' && recheckFn()) {
24114
+ return Promise.resolve();
24115
+ }
24116
+ return new Promise(resolve => {
24117
+ const root = document;
24118
+ const cleanupAll = () => {
24119
+ root.removeEventListener(eventName, onEvent, true);
24120
+ if (mo) mo.disconnect();
24057
24121
  };
24058
- const root = document; // capture at doc to catch composed events
24059
- const cleanup = () => root.removeEventListener('ready', onReady, true);
24060
- root.addEventListener('ready', onReady, true);
24061
-
24062
- // also re-check on DOM changes in case a ready fore is inserted without firing (paranoia)
24063
- const mo = new MutationObserver(() => {
24064
- if (isReadyNow(sel)) {
24065
- mo.disconnect();
24066
- cleanup();
24122
+ const onEvent = ev => {
24123
+ if (matchesEventFn(ev)) {
24124
+ cleanupAll();
24067
24125
  resolve();
24068
24126
  }
24069
- });
24070
- mo.observe(document.documentElement, {
24071
- childList: true,
24072
- subtree: true
24073
- });
24127
+ };
24128
+ root.addEventListener(eventName, onEvent, true);
24129
+
24130
+ // Only used for `ready` (or any other gate that provides a recheck function)
24131
+ let mo = null;
24132
+ if (typeof recheckFn === 'function') {
24133
+ mo = new MutationObserver(() => {
24134
+ if (recheckFn()) {
24135
+ cleanupAll();
24136
+ resolve();
24137
+ }
24138
+ });
24139
+ mo.observe(document.documentElement, {
24140
+ childList: true,
24141
+ subtree: true
24142
+ });
24143
+ }
24074
24144
  });
24075
- return Promise.all(sels.map(waitOne));
24145
+ }
24146
+ _waitForInitGate({
24147
+ event,
24148
+ targetSpec
24149
+ }) {
24150
+ // Direct targets
24151
+ if (targetSpec === 'self') {
24152
+ const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
24153
+ return this._waitForEvent(this, event, satisfied);
24154
+ }
24155
+ if (targetSpec === 'document') {
24156
+ return this._waitForEvent(document, event);
24157
+ }
24158
+ if (targetSpec === 'window') {
24159
+ return this._waitForEvent(window, event);
24160
+ }
24161
+
24162
+ // Special: closest fx-fore
24163
+ if (targetSpec === 'closest') {
24164
+ const recheckFn = event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
24165
+ const matchesFn = ev => {
24166
+ const t = ev.target;
24167
+ return t?.tagName === 'FX-FORE' && t.contains(this);
24168
+ };
24169
+ return this._waitForMatchingEvent(event, matchesFn, recheckFn);
24170
+ }
24171
+
24172
+ // Selector targets
24173
+ const selector = targetSpec;
24174
+ const recheckFn = event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
24175
+ if (typeof recheckFn === 'function' && recheckFn()) {
24176
+ return Promise.resolve();
24177
+ }
24178
+ const matchesFn = ev => {
24179
+ // Prefer composedPath() so events coming from inside shadow DOM still match
24180
+ const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
24181
+ for (const n of path) {
24182
+ if (n && n.matches && n.matches(selector)) return true;
24183
+ }
24184
+ const t = ev.target;
24185
+ return !!(t && t.closest && t.closest(selector));
24186
+ };
24187
+ return this._waitForMatchingEvent(event, matchesFn, recheckFn);
24188
+ }
24189
+
24190
+ /**
24191
+ * Wait until all configured init gates are satisfied.
24192
+ * This is the single consolidation point for init gating.
24193
+ */
24194
+ _waitForInitGates() {
24195
+ if (this._initGatesPromise) return this._initGatesPromise;
24196
+ const gates = this._collectInitGates();
24197
+ if (!gates.length) {
24198
+ this._initGatesPromise = Promise.resolve();
24199
+ return this._initGatesPromise;
24200
+ }
24201
+ this._initGatesPromise = Promise.all(gates.map(g => this._waitForInitGate(g))).then(() => undefined);
24202
+ return this._initGatesPromise;
24203
+ }
24204
+ attributeChangedCallback(name, oldValue, newValue) {
24205
+ if (oldValue === newValue) return;
24206
+ if (name === 'src') {
24207
+ this.src = newValue;
24208
+ if (!newValue) {
24209
+ // Reset so a later src assignment can load again
24210
+ this._srcLoadPromise = null;
24211
+ return;
24212
+ }
24213
+ if (this.isConnected) {
24214
+ this._maybeLoadFromSrc();
24215
+ }
24216
+ return;
24217
+ }
24218
+ if (name === 'selector') {
24219
+ // Selector changes should affect a pending src-load
24220
+ if (this.isConnected && this.src && !this._srcLoadPromise) {
24221
+ this._maybeLoadFromSrc();
24222
+ }
24223
+ }
24224
+ }
24225
+ _maybeLoadFromSrc() {
24226
+ if (!this.src) return null;
24227
+ if (this._srcLoadPromise) return this._srcLoadPromise;
24228
+ this._srcLoadPromise = (async () => {
24229
+ await this._waitForInitGates();
24230
+ if (!this.isConnected) return;
24231
+ const selector = this.getAttribute('selector') || 'fx-fore';
24232
+ await Fore.loadForeFromSrc(this, this.src, selector);
24233
+ })();
24234
+ return this._srcLoadPromise;
24076
24235
  }
24077
24236
  connectedCallback() {
24078
24237
  const modelElement = Array.from(this.children).find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');
@@ -24110,7 +24269,7 @@ class FxFore extends HTMLElement {
24110
24269
  }
24111
24270
  this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
24112
24271
  if (this.src) {
24113
- this._loadFromSrc();
24272
+ this._maybeLoadFromSrc();
24114
24273
  return;
24115
24274
  }
24116
24275
  this._injectDevtools();
@@ -24186,15 +24345,7 @@ class FxFore extends HTMLElement {
24186
24345
  * @private
24187
24346
  */
24188
24347
  async _loadFromSrc() {
24189
- // console.log('########## loading Fore from ', this.src, '##########');
24190
- if (this.hasAttribute('wait-for')) {
24191
- await this._whenDependenciesReady();
24192
- }
24193
- if (this.hasAttribute('selector')) {
24194
- await Fore.loadForeFromSrc(this, this.src, this.getAttribute('selector'));
24195
- } else {
24196
- await Fore.loadForeFromSrc(this, this.src, 'fx-fore');
24197
- }
24348
+ return this._maybeLoadFromSrc();
24198
24349
  }
24199
24350
 
24200
24351
  /**
@@ -24489,7 +24640,7 @@ class FxFore extends HTMLElement {
24489
24640
  }
24490
24641
  // Templates are special: they use the namespace configuration from the place where they are
24491
24642
  // being defined
24492
- const instanceId = XPathUtil.getInstanceId(naked);
24643
+ const instanceId = XPathUtil.getInstanceId(naked, node);
24493
24644
 
24494
24645
  // If there is an instance referred
24495
24646
  const inst = instanceId ? this.getModel().getInstance(instanceId) : this.getModel().getDefaultInstance();
@@ -24969,11 +25120,13 @@ class FxFore extends HTMLElement {
24969
25120
  let newElement;
24970
25121
  if (ref.includes('/')) {
24971
25122
  // multi-step ref expressions
24972
- newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
25123
+ const namespaceResolver = createNamespaceResolver(ref, this);
25124
+ newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
24973
25125
  // console.log('new subtree', newElement);
24974
25126
  return newElement;
24975
25127
  } else {
24976
- return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
25128
+ const namespaceResolver = createNamespaceResolver(ref, this);
25129
+ return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
24977
25130
  }
24978
25131
  }
24979
25132
  _handleDragStart(event) {
@@ -25132,6 +25285,7 @@ class FxFore extends HTMLElement {
25132
25285
  }
25133
25286
  FxFore.outermostHandler = null;
25134
25287
  FxFore.draggedItem = null;
25288
+ FxFore._initEventState = new WeakMap();
25135
25289
  if (!customElements.get('fx-fore')) {
25136
25290
  customElements.define('fx-fore', FxFore);
25137
25291
  }
@@ -25582,8 +25736,27 @@ class FxSubmission extends ForeElementMixin {
25582
25736
  _handleResponse(data, resolvedUrl, contentType) {
25583
25737
  // console.log('_handleResponse ', data);
25584
25738
 
25585
- const targetInstance = this._getTargetInstance();
25739
+ this._getTargetInstance();
25586
25740
  if (this.replace === 'instance') {
25741
+ const targetInstance = this._getTargetInstance();
25742
+
25743
+ // ### contentType handling
25744
+
25745
+ if (contentType.includes('html')) {
25746
+ let effectiveData;
25747
+ if (data.nodeType) {
25748
+ effectiveData = data;
25749
+ }
25750
+ // ## try parsing
25751
+ try {
25752
+ effectiveData = new DOMParser().parseFromString(data, 'text/html');
25753
+ } catch {
25754
+ Fore.dispatch(this, 'error', {
25755
+ message: 'could not parse data as HTML'
25756
+ });
25757
+ }
25758
+ targetInstance.instanceData = effectiveData;
25759
+ }
25587
25760
  if (targetInstance) {
25588
25761
  if (this.targetref) {
25589
25762
  const [theTarget] = evaluateXPath(this.targetref, targetInstance.instanceData.firstElementChild, this);
@@ -26070,6 +26243,9 @@ class AbstractControl extends UIElement {
26070
26243
  }
26071
26244
  }
26072
26245
  _toggleValid(valid) {
26246
+ // Used by required handling (and potentially other callers).
26247
+ // It must also fire validity events and sync aria-invalid.
26248
+ const wasInvalid = this.hasAttribute('invalid');
26073
26249
  if (valid) {
26074
26250
  this.removeAttribute('invalid');
26075
26251
  this.setAttribute('valid', '');
@@ -26077,6 +26253,23 @@ class AbstractControl extends UIElement {
26077
26253
  this.removeAttribute('valid');
26078
26254
  this.setAttribute('invalid', '');
26079
26255
  }
26256
+ this._syncAriaInvalid();
26257
+ const isInvalid = this.hasAttribute('invalid');
26258
+ // Only dispatch when the state actually changed
26259
+ if (wasInvalid !== isInvalid) {
26260
+ this._dispatchEvent(isInvalid ? 'invalid' : 'valid');
26261
+ }
26262
+ }
26263
+ _syncAriaInvalid() {
26264
+ // Keep widget aria-invalid in sync with the *control* state, regardless of
26265
+ // whether invalidity comes from constraint, required emptiness, etc.
26266
+ try {
26267
+ const w = this.getWidget?.() || this.widget;
26268
+ if (!w) return;
26269
+ w.setAttribute('aria-invalid', this.hasAttribute('invalid') ? 'true' : 'false');
26270
+ } catch (e) {
26271
+ // ignore: widget might not exist yet
26272
+ }
26080
26273
  }
26081
26274
  handleReadonly() {
26082
26275
  // console.log('mip readonly', this.modelItem.isReadonly);
@@ -26104,8 +26297,8 @@ class AbstractControl extends UIElement {
26104
26297
  // if (alert) alert.style.display = 'none';
26105
26298
  this._dispatchEvent('valid');
26106
26299
  this.setAttribute('valid', '');
26107
- this.getWidget().setAttribute('aria-invalid', 'false');
26108
26300
  this.removeAttribute('invalid');
26301
+ this.getWidget().setAttribute('aria-invalid', 'false');
26109
26302
  } else {
26110
26303
  this.setAttribute('invalid', '');
26111
26304
  this.getWidget().setAttribute('aria-invalid', 'true');
@@ -26137,33 +26330,40 @@ class AbstractControl extends UIElement {
26137
26330
  this._dispatchEvent('invalid');
26138
26331
  }
26139
26332
  }
26333
+
26334
+ // Ensure aria-invalid matches the current control state even if
26335
+ // we didn't enter the state-change branch above.
26336
+ this._syncAriaInvalid();
26140
26337
  }
26141
26338
  handleRelevant() {
26142
- // console.log('mip valid', this.modelItem.enabled);
26339
+ // IMPORTANT: don't clear relevant/nonrelevant BEFORE comparing states.
26340
+ // Otherwise isEnabled() (based on attributes) always reads as "enabled"
26341
+ // and we can never detect a transition back to relevant.
26143
26342
  const item = this.modelItem.node;
26144
- this.removeAttribute('relevant');
26145
- this.removeAttribute('nonrelevant');
26343
+ const wasEnabled = this.isEnabled();
26344
+
26345
+ // Determine new enabled state
26346
+ let newEnabled = !!this.modelItem.relevant;
26347
+
26348
+ // If a nodeset resolves to an empty array, treat the control as nonrelevant
26146
26349
  if (Array.isArray(item) && item.length === 0) {
26147
- this._dispatchEvent('nonrelevant');
26350
+ newEnabled = false;
26351
+ }
26352
+
26353
+ // Apply attributes
26354
+ if (newEnabled) {
26355
+ this.setAttribute('relevant', '');
26356
+ this.removeAttribute('nonrelevant');
26357
+ } else {
26148
26358
  this.setAttribute('nonrelevant', '');
26149
- // this.style.display = 'none';
26150
- return;
26359
+ this.removeAttribute('relevant');
26151
26360
  }
26152
- if (this.isEnabled() !== this.modelItem.relevant) {
26153
- if (this.modelItem.relevant) {
26154
- this._dispatchEvent('relevant');
26155
- // this._fadeIn(this, this.display);
26156
- this.setAttribute('relevant', '');
26157
- // this.style.display = this.display;
26158
- } else {
26159
- this._dispatchEvent('nonrelevant');
26160
- // this._fadeOut(this);
26161
- this.setAttribute('nonrelevant', '');
26162
- // this.style.display = 'none';
26163
- }
26361
+
26362
+ // Dispatch only on actual change
26363
+ if (wasEnabled !== newEnabled) {
26364
+ this._dispatchEvent(newEnabled ? 'relevant' : 'nonrelevant');
26164
26365
  }
26165
26366
  }
26166
-
26167
26367
  isRequired() {
26168
26368
  return this.hasAttribute('required');
26169
26369
  }
@@ -30212,7 +30412,7 @@ class FxActionLog extends HTMLElement {
30212
30412
  */
30213
30413
  _logDetails(e) {
30214
30414
  const eventType = e.type;
30215
- const path = XPathUtil.getPath(e.target, '');
30415
+ const path = getPath(e.target, '');
30216
30416
  // console.log('>>>> _logDetails', path);
30217
30417
  const cut = path.substring(path.indexOf('/fx-fore'), path.length);
30218
30418
  const xpath = `/${cut}`;
@@ -36035,6 +36235,14 @@ function registerFunction(functionObject, formElement) {
36035
36235
  switch (type) {
36036
36236
  case 'text/javascript':
36037
36237
  {
36238
+ // NEW: if a real JS function is provided (module libs), register it directly.
36239
+ if (typeof functionObject.implementation === 'function') {
36240
+ const impl = functionObject.implementation;
36241
+ registerCustomXPathFunction(functionIdentifier, paramParts.map(paramPart => paramPart.variableType), returnType || 'item()*', (domFacade, ...values) => impl.apply(formElement.getInScopeContext(), [...values, formElement.getOwnerForm()]));
36242
+ break;
36243
+ }
36244
+
36245
+ // Existing behavior: compile from functionBody
36038
36246
  // eslint-disable-next-line no-new-func
36039
36247
  const fun = new Function('_domFacade', ...paramParts.map(paramPart => paramPart.variableName), 'form', functionObject.functionBody);
36040
36248
  registerCustomXPathFunction(functionIdentifier, paramParts.map(paramPart => paramPart.variableType), returnType || 'item()*', (...args) => fun.apply(formElement.getInScopeContext(), [...args, formElement.getOwnerForm()]));
@@ -36085,10 +36293,35 @@ if (!customElements.get('fx-function')) {
36085
36293
  customElements.define('fx-function', FxFunction);
36086
36294
  }
36087
36295
 
36088
- /**
36089
- * Allows to extend a form with remote custom functions.
36090
- *
36091
- */
36296
+ const LOCAL_FUNCTIONS_NS = 'http://www.w3.org/2005/xquery-local-functions';
36297
+
36298
+ // Global per-page cache to prevent registering the same library multiple times.
36299
+ // Keyed by resolved URL + prefix + mode.
36300
+ const _functionLibLoadCache = new Map();
36301
+ function looksLikeModuleSrc(src) {
36302
+ return /\.m?js($|\?)/i.test(src);
36303
+ }
36304
+ function applyPrefixToSignature(signature, prefix) {
36305
+ if (!signature || !prefix) return signature;
36306
+ const s = signature.trim();
36307
+ const paren = s.indexOf('(');
36308
+ if (paren < 0) return s;
36309
+ const namePart = s.slice(0, paren).trim();
36310
+ const rest = s.slice(paren);
36311
+ const localName = namePart.includes(':') ? namePart.split(':').pop().trim() : namePart;
36312
+ if (!localName) return s;
36313
+ return `${prefix}:${localName}${rest}`;
36314
+ }
36315
+ function normalizeModuleExportToList(mod, src) {
36316
+ const lib = mod.functions ?? mod.fxFunctions;
36317
+ if (!lib) {
36318
+ console.error(`fx-functionlib: Module ${src} must export a named \`functions\` (or \`fxFunctions\`).`);
36319
+ return [];
36320
+ }
36321
+ if (Array.isArray(lib)) return lib;
36322
+ if (typeof lib === 'object') return Object.values(lib);
36323
+ return [];
36324
+ }
36092
36325
  class FxFunctionlib extends ForeElementMixin {
36093
36326
  constructor() {
36094
36327
  super();
@@ -36106,27 +36339,98 @@ class FxFunctionlib extends ForeElementMixin {
36106
36339
  async connectedCallback() {
36107
36340
  this.style.display = 'none';
36108
36341
  const src = this.getAttribute('src');
36109
- const result = await fetch(src);
36342
+ if (!src) {
36343
+ console.error('fx-functionlib: Missing required @src.');
36344
+ this._resolveLoading(undefined);
36345
+ return;
36346
+ }
36347
+ const prefix = (this.getAttribute('prefix') || '').trim();
36348
+ const typeAttr = (this.getAttribute('type') || '').trim().toLowerCase();
36349
+ const isModule = typeAttr === 'module' || !typeAttr && looksLikeModuleSrc(src);
36350
+ const resolvedUrl = new URL(src, this.baseURI).href;
36351
+ if (prefix) this._ensurePrefixDeclared(prefix);
36352
+ const mode = isModule ? 'module' : 'html';
36353
+ const cacheKey = `${mode}|${resolvedUrl}|${prefix}`;
36354
+ const existing = _functionLibLoadCache.get(cacheKey);
36355
+ if (existing) {
36356
+ try {
36357
+ await existing;
36358
+ } finally {
36359
+ this._resolveLoading(undefined);
36360
+ }
36361
+ return;
36362
+ }
36363
+ const loadPromise = (async () => {
36364
+ if (isModule) {
36365
+ await this._loadModuleLibrary(resolvedUrl, src, prefix);
36366
+ } else {
36367
+ await this._loadHtmlLibrary(resolvedUrl, src, prefix);
36368
+ }
36369
+ })();
36370
+ _functionLibLoadCache.set(cacheKey, loadPromise);
36371
+ try {
36372
+ await loadPromise;
36373
+ } catch (e) {
36374
+ _functionLibLoadCache.delete(cacheKey);
36375
+ console.error(`fx-functionlib: Loading function library at ${src} failed.`, e);
36376
+ } finally {
36377
+ this._resolveLoading(undefined);
36378
+ }
36379
+ }
36380
+ _ensurePrefixDeclared(prefix) {
36381
+ const ownerForm = typeof this.getOwnerForm === 'function' && this.getOwnerForm() || this.closest('fx-fore');
36382
+ if (!ownerForm) return;
36383
+ const attrName = `xmlns:${prefix}`;
36384
+ if (!ownerForm.getAttribute(attrName)) {
36385
+ ownerForm.setAttribute(attrName, LOCAL_FUNCTIONS_NS);
36386
+ }
36387
+ }
36388
+ _register(functionObject, prefix) {
36389
+ if (!functionObject || typeof functionObject.signature !== 'string') return;
36390
+
36391
+ // If prefix is given: register ONLY the prefixed signature (no unprefixed alias).
36392
+ const sig = prefix ? applyPrefixToSignature(functionObject.signature, prefix) : functionObject.signature;
36393
+ registerFunction({
36394
+ ...functionObject,
36395
+ signature: sig
36396
+ }, this);
36397
+ }
36398
+ async _loadModuleLibrary(resolvedUrl, src, prefix) {
36399
+ const mod = await import( /* @vite-ignore */resolvedUrl);
36400
+ const items = normalizeModuleExportToList(mod, src);
36401
+ for (const item of items) {
36402
+ if (typeof item === 'function') {
36403
+ const {
36404
+ signature
36405
+ } = item;
36406
+ if (typeof signature !== 'string' || !signature.trim()) continue;
36407
+ this._register({
36408
+ type: 'text/javascript',
36409
+ signature: signature.trim(),
36410
+ implementation: item
36411
+ }, prefix);
36412
+ } else if (item && typeof item === 'object' && typeof item.signature === 'string') {
36413
+ this._register(item, prefix);
36414
+ }
36415
+ }
36416
+ }
36417
+ async _loadHtmlLibrary(resolvedUrl, src, prefix) {
36418
+ const result = await fetch(resolvedUrl);
36110
36419
  if (!result.ok) {
36111
36420
  console.error(`Loading function library at ${src} failed.`);
36421
+ return;
36112
36422
  }
36113
36423
  const body = await result.text();
36114
36424
  const document = new DOMParser().parseFromString(body, 'text/html');
36115
-
36116
- /**
36117
- * @type {HTMLElement[]}
36118
- */
36119
36425
  const functions = Array.from(document.querySelectorAll('fx-function'));
36120
- // TODO: also recurse into new function libraries here?
36121
36426
  for (const func of functions) {
36122
36427
  const functionObject = {
36123
36428
  type: func.getAttribute('type'),
36124
36429
  signature: func.getAttribute('signature'),
36125
36430
  functionBody: func.innerText
36126
36431
  };
36127
- registerFunction(functionObject, this);
36432
+ this._register(functionObject, prefix);
36128
36433
  }
36129
- this._resolveLoading(undefined);
36130
36434
  }
36131
36435
  }
36132
36436
  if (!customElements.get('fx-functionlib')) {