@jinntec/fore 2.7.2 → 2.8.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.8.0 - December 19, 2025 12:59:03 */
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
+ }
16974
16956
  }
16957
+ if (!rootNode) {
16958
+ throw new Error('Invalid XPath; no root element could be created.');
16959
+ }
16960
+ return rootNode;
16975
16961
  }
16976
- // We found no target objects in common repeats. The id is unresolvable
16977
- return null;
16978
- }
16979
-
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 [];
16987
- }
16988
- if (instanceReferencesByQuery.has(xpathQuery)) {
16989
- return instanceReferencesByQuery.get(xpathQuery);
16990
- }
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
16962
 
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;
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}`);
17026
16976
  }
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;
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
+ }
17035
16991
  }
17036
- // Nothing found: let's just assume we're supposed to use the `default` instance
17037
- instanceReferences = ['default'];
16992
+ return found;
17038
16993
  }
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;
16994
+
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
+ }
17071
17012
  }
17013
+ return false;
17072
17014
  }
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
17015
 
17081
- const xpathDefaultNamespace = evaluateXPathToString$1('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) || '';
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
+ }
17046
+ }
17047
+ return start;
17048
+ }
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
17215
17193
  *
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
17194
+ * based on the work of https://github.com/jriecken/dependency-graph but working on ES6.
17221
17195
  *
17222
- * @returns {any[]}
17196
+ * Furthermore instead of the DepGraphCycleError a compute-exception event is dispatched.
17197
+ *
17198
+ *
17199
+ */
17200
+
17201
+ /**
17202
+ * Cycle error, including the path of the cycle.
17223
17203
  */
17204
+ // const DepGraphCycleError = (exports.DepGraphCycleError = function (cyclePath) {
17205
+
17224
17206
  /*
17225
- export function evaluateXPath(xpath, contextNode, formElement, variables = {}, options={}, domFacade = null) {
17226
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
17227
- const variablesInScope = getVariablesInScope(formElement);
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);
17214
+ }
17215
+ return instance;
17216
+ };
17228
17217
 
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
- }
17218
+ DepGraphCycleError.prototype = Object.create(Error.prototype, {
17219
+ constructor: {
17220
+ value: Error,
17221
+ enumerable: false,
17222
+ writable: true,
17223
+ configurable: true
17224
+ }
17225
+ });
17226
+ Object.setPrototypeOf(DepGraphCycleError, Error);
17247
17227
  */
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
17228
 
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
17229
  /**
17302
- * Evaluate an XPath to the first Node
17230
+ * Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
17303
17231
  *
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
17308
- */
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
- }));
17338
- }
17339
- return null;
17340
- }
17341
-
17342
- /**
17343
- * Evaluate an XPath to all nodes
17232
+ * Detects cycles and throws an Error if one is detected (unless the "circular"
17233
+ * parameter is "true" in which case it ignores them).
17344
17234
  *
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
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
17349
17239
  */
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()
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
17364
17254
  });
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
- }));
17378
- }
17379
- }
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);
17380
17289
 
17381
- /**
17382
- * Evaluate an XPath to a boolean
17383
- *
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}
17388
- */
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()
17403
- });
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'
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
- }
17456
- }
17457
17321
 
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'
17492
- }
17493
- }));
17494
- }
17495
- }
17496
-
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()
17522
- });
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'
17532
- }
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
- }
17554
- }
17555
- }
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
-
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);
17597
-
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);
17617
- }
17618
- }
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);
17645
-
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;
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
17660
17328
  });
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
- } */
17329
+ */
17705
17330
 
17706
- // return tree;
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
17707
17336
  }
17708
17337
 
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());
17720
-
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
- }
17735
- }
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
-
17338
+ DepGraph.prototype = {
17742
17339
  /**
17743
- * @type {import('./fx-fore.js').FxFore}
17340
+ * The number of nodes in the graph.
17744
17341
  */
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
- }
17764
- }
17765
- const context = lookup.getDefaultContext();
17766
- if (!context) {
17767
- return null;
17768
- }
17769
- return context;
17770
- };
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
-
17783
- // const def = instance.getInstanceData();
17784
- if (repeat) {
17785
- return repeat.getAttribute('index');
17786
- }
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');
17840
- }
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
- }
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;
17873
17355
  }
17874
- return obj;
17356
+ this.outgoingEdges[node] = [];
17357
+ this.incomingEdges[node] = [];
17875
17358
  }
17876
- if (isTextNode(node)) {
17877
- return node.textContent;
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
+ });
17878
17377
  }
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);
17890
-
17891
- /*
17892
- // Example usage:
17893
- const xml = '<json type="object"><given>Mark</given><family>Smith</family></json>';
17894
- console.log(xmlToJson(xml));
17895
- */
17378
+ },
17379
+ /**
17380
+ * Check if a node exists in the graph
17381
+ */
17382
+ hasNode(node) {
17383
+ // return this.nodes.hasOwnProperty(node);
17896
17384
 
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;
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];
17911
17393
  }
17912
-
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];
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}`);
17918
17404
  }
17919
-
17920
- // arg might be `code`, so currentEvent.code should work
17921
- if (arg.includes('.')) {
17922
- return _propertyLookup(ancestor.currentEvent, arg);
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}`);
17923
17413
  }
17924
- return ancestor.currentEvent[arg] || null;
17925
- }
17926
- 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
- }
17935
-
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
-
17968
- /**
17969
- * @param input as string
17970
- * @return {string}
17971
- */
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
-
18039
- /**
18040
- * @param {Node} node
18041
- * @returns string
18042
- */
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 {
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);
17455
+ });
17456
+ return result;
17457
+ },
18053
17458
  /**
18054
- * Recursively check AST for any dynamic expression components.
17459
+ * Get an array containing the direct dependencies of the specified node.
17460
+ *
17461
+ * Throws an Error if the specified node does not exist.
18055
17462
  */
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;
17463
+ directDependenciesOf(node) {
17464
+ if (this.hasNode(node)) {
17465
+ return this.outgoingEdges[node].slice(0);
18062
17466
  }
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;
18073
- }
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);
18074
17496
  }
17497
+ return result;
18075
17498
  }
18076
- return false;
18077
- }
18078
-
17499
+ throw new Error(`Node does not exist: ${node}`);
17500
+ },
18079
17501
  /**
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
17502
+ * get an array containing the nodes that depend on the specified node (transitively).
18084
17503
  *
18085
- * supports multiple steps
17504
+ * Throws an Error if the graph has a cycle, or the specified node does not exist.
18086
17505
  *
18087
- * @param xpath
18088
- * @param doc {XMLDocument}
18089
- * @param fore
18090
- * @return {Node|Attr}
17506
+ * If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
18091
17507
  */
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
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);
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
18096
17541
  }
18097
17542
 
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
- }
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
+ });
17557
+
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));
17563
+ }
17564
+ return result;
17565
+ }
17566
+ };
17567
+
17568
+ // Create some aliases
17569
+ DepGraph.prototype.directDependentsOf = DepGraph.prototype.directDependantsOf;
17570
+ DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
17571
+
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;
17584
+ }
17585
+
17586
+ const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
17587
+ const createdNamespaceResolversByXPathQueryAndNode = new Map();
17588
+
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;
17595
+ }
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());
17601
+ }
17602
+ return createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
17603
+ }
17604
+ const xhtmlNamespaceResolver = prefix => {
17605
+ if (!prefix) {
17606
+ return 'http://www.w3.org/1999/xhtml';
17607
+ }
17608
+ return undefined;
17609
+ };
17610
+
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);
17631
+ }
17632
+ */
17633
+ if (sourceObject.nodeType === Node.TEXT_NODE) {
17634
+ sourceObject = sourceObject.parentNode;
17635
+ }
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;
17649
+ }
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;
18120
17671
  }
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
- }
17672
+ return targetObject;
17673
+ }
18131
17674
 
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.
17675
+ // SPEC:
18149
17676
 
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);
17677
+ // 12.2.1 References to Elements within a repeat Element
17678
+
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.
17684
+
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;
18173
17703
  }
17704
+ return targetObject;
18174
17705
  }
18175
- if (!rootNode) {
18176
- rootNode = newElement; // Set as the root node
18177
- } else {
18178
- currentNode.appendChild(newElement);
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;
18179
17720
  }
18180
- currentNode = newElement;
18181
- }
18182
- }
18183
- if (!rootNode) {
18184
- throw new Error('Invalid XPath; no root element could be created.');
18185
17721
  }
18186
- return rootNode;
18187
17722
  }
17723
+ // We found no target objects in common repeats. The id is unresolvable
17724
+ return null;
17725
+ }
18188
17726
 
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}`);
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 [];
18202
17734
  }
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);
18210
- }
18211
- if (item.matches && item.matches('template')) {
18212
- queue.unshift(item.content);
18213
- }
18214
- if (item.matches && item.matches(querySelector)) {
18215
- found.push(item);
18216
- }
18217
- }
18218
- return found;
17735
+ if (instanceReferencesByQuery.has(xpathQuery)) {
17736
+ return instanceReferencesByQuery.get(xpathQuery);
18219
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
+ }
17749
+ /**
17750
+ * @typedef {function(string):string} NamespaceResolver
17751
+ */
18220
17752
 
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
- }
17753
+ /**
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
17768
+ */
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;
18238
17782
  }
18239
- return false;
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;
17813
+ }
17814
+ return undefined;
17815
+ };
17816
+ setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
17817
+ return resolveNamespacePrefix;
17818
+ }
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
+ );
18240
17825
  }
17826
+ */
17827
+
17828
+ const xpathDefaultNamespace = evaluateXPathToString$1('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) || '';
18241
17829
 
18242
17830
  /**
18243
- * Alternative to `closest` that respects subcontrol boundaries
18244
- *
18245
- * @param {string} querySelector
18246
- * @param {Node} start
18247
- * @returns {HTMLElement}
17831
+ * @type {NamespaceResolver}
18248
17832
  */
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;
18259
- continue;
18260
- }
18261
- if (start.nodeType === Node.TEXT_NODE) {
18262
- start = start.parentNode;
17833
+ const resolveNamespacePrefix = function resolveNamespacePrefix(prefix) {
17834
+ if (prefix === '') {
17835
+ return xpathDefaultNamespace;
17836
+ }
17837
+
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!
17841
+
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;
17853
+ }
17854
+ return createNamespaceResolver(query, formElement);
17855
+ }
17856
+
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
+ };
18263
17906
  }
18264
- if (start.matches('fx-fore')) {
18265
- // Subform reached. Bail out
18266
- return null;
17907
+ if (prefix === 'fn' || prefix === '') {
17908
+ return {
17909
+ namespaceURI: 'http://www.w3.org/2005/xpath-functions',
17910
+ localName
17911
+ };
18267
17912
  }
18268
- start = start.parentNode;
18269
- if (!start) {
18270
- return null;
17913
+ if (prefix === 'local') {
17914
+ return {
17915
+ namespaceURI: 'http://www.w3.org/2005/xquery-local-functions',
17916
+ localName
17917
+ };
18271
17918
  }
18272
- }
18273
- return start;
17919
+ return null;
18274
17920
  }
17921
+ }
18275
17922
 
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;
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;
18292
17935
  }
18293
-
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('$'));
17936
+ if (!closestActualFormElement) {
17937
+ return {};
18303
17938
  }
18304
-
18305
- /**
18306
- * @param {string} ref
18307
- */
18308
- static isSelfReference(ref) {
18309
- return ref === '.' || ref === './text()' || ref === 'text()' || ref === '' || ref === null;
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) {
17944
+ continue;
17945
+ }
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;
17953
+ }
17954
+ }
18310
17955
  }
17956
+ return variables;
17957
+ }
18311
17958
 
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;
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);
17975
+
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'
18343
18027
  }
18344
- return this.getInstanceId(correspondingVariable.valueQuery, correspondingVariable);
18345
- }
18346
- return null;
18347
- }
18028
+ }));
18348
18029
 
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';
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 [];
18367
18046
  }
18368
-
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}`;
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'
18083
+ }
18084
+ }));
18379
18085
  }
18380
- */
18086
+ return null;
18087
+ }
18381
18088
 
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}`;
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
+ }));
18392
18125
  }
18126
+ }
18393
18127
 
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);
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
+ }));
18405
18162
  }
18163
+ }
18406
18164
 
18407
- /**
18408
- * @param {string} dep
18409
- * @returns {string}
18410
- */
18411
- static getBasePath(dep) {
18412
- const split = dep.split(':');
18413
- return split[0];
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
+ }));
18414
18202
  }
18415
18203
  }
18416
18204
 
18417
18205
  /**
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
- *
18206
+ * Evaluate an XPath to a set of strings
18424
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[]}
18425
18215
  */
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);
18440
- }
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
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
+ }));
18450
18241
  }
18451
- });
18452
- Object.setPrototypeOf(DepGraphCycleError, Error);
18453
- */
18242
+ }
18454
18243
 
18455
18244
  /**
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).
18245
+ * Evaluate an XPath to a number
18460
18246
  *
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
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}
18465
18254
  */
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
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()
18480
18269
  });
18481
- while (todo.length > 0) {
18482
- const current = todo[todo.length - 1]; // peek at the todo stack
18483
- const {
18484
- processed
18485
- } = current;
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
+ }));
18281
+ }
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);
18290
+ }
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
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);
18344
+
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;
18554
18367
  });
18555
- */
18556
-
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
- }
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];
18619
- }
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}`);
18630
- }
18631
- },
18632
- /**
18633
- * Add a dependency between two nodes. If either of the nodes does not exist,
18634
- * an Error will be thrown.
18635
- */
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);
18645
- }
18646
- if (this.incomingEdges[to].indexOf(from) === -1) {
18647
- this.incomingEdges[to].push(from);
18648
- }
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);
18660
- }
18661
- }
18662
- if (this.hasNode(to)) {
18663
- idx = this.incomingEdges[to].indexOf(from);
18664
- if (idx >= 0) {
18665
- this.incomingEdges[to].splice(idx, 1);
18666
- }
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);
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);
18692
18477
  }
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);
18478
+ const tree = buildTree(treeDiv, instance.getDefaultContext());
18479
+ if (tree) {
18480
+ form.appendChild(tree);
18703
18481
  }
18704
- throw new Error(`Node does not exist: ${node}`);
18705
- },
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
+
18706
18489
  /**
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.
18490
+ * @type {import('./fx-fore.js').FxFore}
18713
18491
  */
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);
18722
- }
18723
- return result;
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
+ }));
18724
18510
  }
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);
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
+ });
18580
+ }
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');
18742
18605
  }
18743
- return result;
18606
+ if (node.childNodes.length === 1 && isTextNode(node.firstChild)) {
18607
+ return node.textContent;
18608
+ }
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
+ }
18620
+ }
18621
+ return obj;
18744
18622
  }
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
18623
+ if (isTextNode(node)) {
18624
+ return node.textContent;
18767
18625
  }
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);
18768
18637
 
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
- });
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;
18776
18658
  }
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
18659
 
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));
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];
18789
18665
  }
18790
- return result;
18666
+
18667
+ // arg might be `code`, so currentEvent.code should work
18668
+ if (arg.includes('.')) {
18669
+ return _propertyLookup(ancestor.currentEvent, arg);
18670
+ }
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');
@@ -23837,13 +23865,16 @@ class FxFore extends HTMLElement {
23837
23865
  this._createRepeatsFromAttributes();
23838
23866
  this.inited = true;
23839
23867
  };
23840
- this.version = 'Version: 2.7.2 - built on December 9, 2025 15:22:48';
23868
+ this.version = 'Version: 2.8.0 - built on December 19, 2025 12:59:03';
23841
23869
 
23842
23870
  /**
23843
23871
  * @type {import('./fx-model.js').FxModel}
23844
23872
  */
23845
23873
  this.model = null;
23846
23874
  this.inited = false;
23875
+ this._initGatesPromise = null;
23876
+ this._warnedWaitForDeprecation = false;
23877
+ this._srcLoadPromise = null;
23847
23878
  // this.addEventListener('model-construct-done', this._handleModelConstructDone);
23848
23879
  // todo: refactoring - these should rather go into connectedcallback
23849
23880
  this.addEventListener('message', this._displayMessage);
@@ -23985,94 +24016,217 @@ class FxFore extends HTMLElement {
23985
24016
  }
23986
24017
 
23987
24018
  /**
23988
- * Resolve elements from the `wait-for` attribute.
23989
- * Supports comma-separated CSS selectors and the special value "closest".
24019
+ * Parse a list of target specs.
24020
+ *
24021
+ * We accept both comma- and whitespace-separated lists (for backward compatibility with `wait-for`).
24022
+ * Each token can be:
24023
+ * - "self" (default)
24024
+ * - "closest" (closest fx-fore)
24025
+ * - "document"
24026
+ * - "window"
24027
+ * - a CSS selector (no whitespace)
23990
24028
  */
23991
- _resolveDependencies() {
23992
- const raw = this.getAttribute('wait-for');
24029
+ _parseTargetList(raw) {
23993
24030
  if (!raw) return [];
23994
- const sels = raw.split(',').map(s => s.trim()).filter(Boolean);
24031
+ return raw.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
24032
+ }
24033
+ _findBySelector(sel) {
23995
24034
  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
- }
24035
+ for (const r of roots) {
24036
+ if (r && 'querySelector' in r) {
24037
+ const el = r.querySelector(sel);
24038
+ if (el) return el;
24008
24039
  }
24009
- if (el) out.push(el);
24010
24040
  }
24011
- return out;
24041
+ return null;
24042
+ }
24043
+ _isReadyTarget(el) {
24044
+ return !!(el && (el.ready === true || el.classList && el.classList.contains('fx-ready') || typeof el.hasAttribute === 'function' && el.hasAttribute('ready')));
24012
24045
  }
24013
24046
 
24014
24047
  /**
24015
- * Wait until all dependencies are ready (i.e., they set `ready = true`
24016
- * and dispatch the `ready` event).
24048
+ * Collect all init gates derived from attributes.
24049
+ *
24050
+ * - `wait-for` (DEPRECATED) becomes: init-on="ready" + init-on-target=<list>
24051
+ * - `init-on` / `init-on-target` define a generic event gate
24017
24052
  */
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
- }
24053
+ _collectInitGates() {
24054
+ const gates = [];
24055
+ const waitForRaw = this.getAttribute('wait-for');
24056
+ if (waitForRaw) {
24057
+ if (!this._warnedWaitForDeprecation) {
24058
+ console.warn('[fx-fore] The "wait-for" attribute is deprecated. Use init-on="ready" init-on-target="..." instead.');
24059
+ this._warnedWaitForDeprecation = true;
24060
+ }
24061
+ const deps = this._parseTargetList(waitForRaw);
24062
+ for (const dep of deps) {
24063
+ gates.push({
24064
+ event: 'ready',
24065
+ targetSpec: dep
24066
+ });
24029
24067
  }
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);
24068
+ }
24069
+ const initOn = this.getAttribute('init-on');
24070
+ const initOnTargetRaw = this.getAttribute('init-on-target');
24071
+ if (initOn || initOnTargetRaw) {
24072
+ const eventName = initOn || 'ready';
24073
+ const targets = initOnTargetRaw ? this._parseTargetList(initOnTargetRaw) : ['self'];
24074
+ for (const t of targets) {
24075
+ gates.push({
24076
+ event: eventName,
24077
+ targetSpec: t
24078
+ });
24036
24079
  }
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();
24080
+ }
24081
+ return gates;
24082
+ }
24083
+ _waitForEvent(target, eventName, isSatisfiedFn = null) {
24084
+ // If a caller provides an explicit satisfaction check, honor it first.
24085
+ if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
24086
+ FxFore._markInitEventSeen(target, eventName);
24087
+ return Promise.resolve();
24088
+ }
24043
24089
 
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
- }
24090
+ // Sticky gate: if this event already happened on this target, don't wait again.
24091
+ if (FxFore._hasSeenInitEvent(target, eventName)) {
24092
+ return Promise.resolve();
24093
+ }
24094
+ return new Promise(resolve => {
24095
+ const ac = new AbortController();
24096
+ const on = () => {
24097
+ FxFore._markInitEventSeen(target, eventName);
24098
+ ac.abort();
24099
+ resolve();
24100
+ };
24101
+ target.addEventListener(eventName, on, {
24102
+ once: true,
24103
+ signal: ac.signal
24104
+ });
24105
+ });
24106
+ }
24107
+ _waitForMatchingEvent(eventName, matchesEventFn, recheckFn = null) {
24108
+ if (typeof recheckFn === 'function' && recheckFn()) {
24109
+ return Promise.resolve();
24110
+ }
24111
+ return new Promise(resolve => {
24112
+ const root = document;
24113
+ const cleanupAll = () => {
24114
+ root.removeEventListener(eventName, onEvent, true);
24115
+ if (mo) mo.disconnect();
24057
24116
  };
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();
24117
+ const onEvent = ev => {
24118
+ if (matchesEventFn(ev)) {
24119
+ cleanupAll();
24067
24120
  resolve();
24068
24121
  }
24069
- });
24070
- mo.observe(document.documentElement, {
24071
- childList: true,
24072
- subtree: true
24073
- });
24122
+ };
24123
+ root.addEventListener(eventName, onEvent, true);
24124
+
24125
+ // Only used for `ready` (or any other gate that provides a recheck function)
24126
+ let mo = null;
24127
+ if (typeof recheckFn === 'function') {
24128
+ mo = new MutationObserver(() => {
24129
+ if (recheckFn()) {
24130
+ cleanupAll();
24131
+ resolve();
24132
+ }
24133
+ });
24134
+ mo.observe(document.documentElement, {
24135
+ childList: true,
24136
+ subtree: true
24137
+ });
24138
+ }
24074
24139
  });
24075
- return Promise.all(sels.map(waitOne));
24140
+ }
24141
+ _waitForInitGate({
24142
+ event,
24143
+ targetSpec
24144
+ }) {
24145
+ // Direct targets
24146
+ if (targetSpec === 'self') {
24147
+ const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
24148
+ return this._waitForEvent(this, event, satisfied);
24149
+ }
24150
+ if (targetSpec === 'document') {
24151
+ return this._waitForEvent(document, event);
24152
+ }
24153
+ if (targetSpec === 'window') {
24154
+ return this._waitForEvent(window, event);
24155
+ }
24156
+
24157
+ // Special: closest fx-fore
24158
+ if (targetSpec === 'closest') {
24159
+ const recheckFn = event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
24160
+ const matchesFn = ev => {
24161
+ const t = ev.target;
24162
+ return t?.tagName === 'FX-FORE' && t.contains(this);
24163
+ };
24164
+ return this._waitForMatchingEvent(event, matchesFn, recheckFn);
24165
+ }
24166
+
24167
+ // Selector targets
24168
+ const selector = targetSpec;
24169
+ const recheckFn = event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
24170
+ if (typeof recheckFn === 'function' && recheckFn()) {
24171
+ return Promise.resolve();
24172
+ }
24173
+ const matchesFn = ev => {
24174
+ // Prefer composedPath() so events coming from inside shadow DOM still match
24175
+ const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
24176
+ for (const n of path) {
24177
+ if (n && n.matches && n.matches(selector)) return true;
24178
+ }
24179
+ const t = ev.target;
24180
+ return !!(t && t.closest && t.closest(selector));
24181
+ };
24182
+ return this._waitForMatchingEvent(event, matchesFn, recheckFn);
24183
+ }
24184
+
24185
+ /**
24186
+ * Wait until all configured init gates are satisfied.
24187
+ * This is the single consolidation point for init gating.
24188
+ */
24189
+ _waitForInitGates() {
24190
+ if (this._initGatesPromise) return this._initGatesPromise;
24191
+ const gates = this._collectInitGates();
24192
+ if (!gates.length) {
24193
+ this._initGatesPromise = Promise.resolve();
24194
+ return this._initGatesPromise;
24195
+ }
24196
+ this._initGatesPromise = Promise.all(gates.map(g => this._waitForInitGate(g))).then(() => undefined);
24197
+ return this._initGatesPromise;
24198
+ }
24199
+ attributeChangedCallback(name, oldValue, newValue) {
24200
+ if (oldValue === newValue) return;
24201
+ if (name === 'src') {
24202
+ this.src = newValue;
24203
+ if (!newValue) {
24204
+ // Reset so a later src assignment can load again
24205
+ this._srcLoadPromise = null;
24206
+ return;
24207
+ }
24208
+ if (this.isConnected) {
24209
+ this._maybeLoadFromSrc();
24210
+ }
24211
+ return;
24212
+ }
24213
+ if (name === 'selector') {
24214
+ // Selector changes should affect a pending src-load
24215
+ if (this.isConnected && this.src && !this._srcLoadPromise) {
24216
+ this._maybeLoadFromSrc();
24217
+ }
24218
+ }
24219
+ }
24220
+ _maybeLoadFromSrc() {
24221
+ if (!this.src) return null;
24222
+ if (this._srcLoadPromise) return this._srcLoadPromise;
24223
+ this._srcLoadPromise = (async () => {
24224
+ await this._waitForInitGates();
24225
+ if (!this.isConnected) return;
24226
+ const selector = this.getAttribute('selector') || 'fx-fore';
24227
+ await Fore.loadForeFromSrc(this, this.src, selector);
24228
+ })();
24229
+ return this._srcLoadPromise;
24076
24230
  }
24077
24231
  connectedCallback() {
24078
24232
  const modelElement = Array.from(this.children).find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');
@@ -24110,7 +24264,7 @@ class FxFore extends HTMLElement {
24110
24264
  }
24111
24265
  this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
24112
24266
  if (this.src) {
24113
- this._loadFromSrc();
24267
+ this._maybeLoadFromSrc();
24114
24268
  return;
24115
24269
  }
24116
24270
  this._injectDevtools();
@@ -24186,15 +24340,7 @@ class FxFore extends HTMLElement {
24186
24340
  * @private
24187
24341
  */
24188
24342
  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
- }
24343
+ return this._maybeLoadFromSrc();
24198
24344
  }
24199
24345
 
24200
24346
  /**
@@ -24489,7 +24635,7 @@ class FxFore extends HTMLElement {
24489
24635
  }
24490
24636
  // Templates are special: they use the namespace configuration from the place where they are
24491
24637
  // being defined
24492
- const instanceId = XPathUtil.getInstanceId(naked);
24638
+ const instanceId = XPathUtil.getInstanceId(naked, node);
24493
24639
 
24494
24640
  // If there is an instance referred
24495
24641
  const inst = instanceId ? this.getModel().getInstance(instanceId) : this.getModel().getDefaultInstance();
@@ -24969,11 +25115,13 @@ class FxFore extends HTMLElement {
24969
25115
  let newElement;
24970
25116
  if (ref.includes('/')) {
24971
25117
  // multi-step ref expressions
24972
- newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
25118
+ const namespaceResolver = createNamespaceResolver(ref, this);
25119
+ newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
24973
25120
  // console.log('new subtree', newElement);
24974
25121
  return newElement;
24975
25122
  } else {
24976
- return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
25123
+ const namespaceResolver = createNamespaceResolver(ref, this);
25124
+ return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
24977
25125
  }
24978
25126
  }
24979
25127
  _handleDragStart(event) {
@@ -25132,6 +25280,7 @@ class FxFore extends HTMLElement {
25132
25280
  }
25133
25281
  FxFore.outermostHandler = null;
25134
25282
  FxFore.draggedItem = null;
25283
+ FxFore._initEventState = new WeakMap();
25135
25284
  if (!customElements.get('fx-fore')) {
25136
25285
  customElements.define('fx-fore', FxFore);
25137
25286
  }
@@ -25582,8 +25731,27 @@ class FxSubmission extends ForeElementMixin {
25582
25731
  _handleResponse(data, resolvedUrl, contentType) {
25583
25732
  // console.log('_handleResponse ', data);
25584
25733
 
25585
- const targetInstance = this._getTargetInstance();
25734
+ this._getTargetInstance();
25586
25735
  if (this.replace === 'instance') {
25736
+ const targetInstance = this._getTargetInstance();
25737
+
25738
+ // ### contentType handling
25739
+
25740
+ if (contentType.includes('html')) {
25741
+ let effectiveData;
25742
+ if (data.nodeType) {
25743
+ effectiveData = data;
25744
+ }
25745
+ // ## try parsing
25746
+ try {
25747
+ effectiveData = new DOMParser().parseFromString(data, 'text/html');
25748
+ } catch {
25749
+ Fore.dispatch(this, 'error', {
25750
+ message: 'could not parse data as HTML'
25751
+ });
25752
+ }
25753
+ targetInstance.instanceData = effectiveData;
25754
+ }
25587
25755
  if (targetInstance) {
25588
25756
  if (this.targetref) {
25589
25757
  const [theTarget] = evaluateXPath(this.targetref, targetInstance.instanceData.firstElementChild, this);
@@ -26070,6 +26238,9 @@ class AbstractControl extends UIElement {
26070
26238
  }
26071
26239
  }
26072
26240
  _toggleValid(valid) {
26241
+ // Used by required handling (and potentially other callers).
26242
+ // It must also fire validity events and sync aria-invalid.
26243
+ const wasInvalid = this.hasAttribute('invalid');
26073
26244
  if (valid) {
26074
26245
  this.removeAttribute('invalid');
26075
26246
  this.setAttribute('valid', '');
@@ -26077,6 +26248,23 @@ class AbstractControl extends UIElement {
26077
26248
  this.removeAttribute('valid');
26078
26249
  this.setAttribute('invalid', '');
26079
26250
  }
26251
+ this._syncAriaInvalid();
26252
+ const isInvalid = this.hasAttribute('invalid');
26253
+ // Only dispatch when the state actually changed
26254
+ if (wasInvalid !== isInvalid) {
26255
+ this._dispatchEvent(isInvalid ? 'invalid' : 'valid');
26256
+ }
26257
+ }
26258
+ _syncAriaInvalid() {
26259
+ // Keep widget aria-invalid in sync with the *control* state, regardless of
26260
+ // whether invalidity comes from constraint, required emptiness, etc.
26261
+ try {
26262
+ const w = this.getWidget?.() || this.widget;
26263
+ if (!w) return;
26264
+ w.setAttribute('aria-invalid', this.hasAttribute('invalid') ? 'true' : 'false');
26265
+ } catch (e) {
26266
+ // ignore: widget might not exist yet
26267
+ }
26080
26268
  }
26081
26269
  handleReadonly() {
26082
26270
  // console.log('mip readonly', this.modelItem.isReadonly);
@@ -26104,8 +26292,8 @@ class AbstractControl extends UIElement {
26104
26292
  // if (alert) alert.style.display = 'none';
26105
26293
  this._dispatchEvent('valid');
26106
26294
  this.setAttribute('valid', '');
26107
- this.getWidget().setAttribute('aria-invalid', 'false');
26108
26295
  this.removeAttribute('invalid');
26296
+ this.getWidget().setAttribute('aria-invalid', 'false');
26109
26297
  } else {
26110
26298
  this.setAttribute('invalid', '');
26111
26299
  this.getWidget().setAttribute('aria-invalid', 'true');
@@ -26137,33 +26325,40 @@ class AbstractControl extends UIElement {
26137
26325
  this._dispatchEvent('invalid');
26138
26326
  }
26139
26327
  }
26328
+
26329
+ // Ensure aria-invalid matches the current control state even if
26330
+ // we didn't enter the state-change branch above.
26331
+ this._syncAriaInvalid();
26140
26332
  }
26141
26333
  handleRelevant() {
26142
- // console.log('mip valid', this.modelItem.enabled);
26334
+ // IMPORTANT: don't clear relevant/nonrelevant BEFORE comparing states.
26335
+ // Otherwise isEnabled() (based on attributes) always reads as "enabled"
26336
+ // and we can never detect a transition back to relevant.
26143
26337
  const item = this.modelItem.node;
26144
- this.removeAttribute('relevant');
26145
- this.removeAttribute('nonrelevant');
26338
+ const wasEnabled = this.isEnabled();
26339
+
26340
+ // Determine new enabled state
26341
+ let newEnabled = !!this.modelItem.relevant;
26342
+
26343
+ // If a nodeset resolves to an empty array, treat the control as nonrelevant
26146
26344
  if (Array.isArray(item) && item.length === 0) {
26147
- this._dispatchEvent('nonrelevant');
26345
+ newEnabled = false;
26346
+ }
26347
+
26348
+ // Apply attributes
26349
+ if (newEnabled) {
26350
+ this.setAttribute('relevant', '');
26351
+ this.removeAttribute('nonrelevant');
26352
+ } else {
26148
26353
  this.setAttribute('nonrelevant', '');
26149
- // this.style.display = 'none';
26150
- return;
26354
+ this.removeAttribute('relevant');
26151
26355
  }
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
- }
26356
+
26357
+ // Dispatch only on actual change
26358
+ if (wasEnabled !== newEnabled) {
26359
+ this._dispatchEvent(newEnabled ? 'relevant' : 'nonrelevant');
26164
26360
  }
26165
26361
  }
26166
-
26167
26362
  isRequired() {
26168
26363
  return this.hasAttribute('required');
26169
26364
  }
@@ -30212,7 +30407,7 @@ class FxActionLog extends HTMLElement {
30212
30407
  */
30213
30408
  _logDetails(e) {
30214
30409
  const eventType = e.type;
30215
- const path = XPathUtil.getPath(e.target, '');
30410
+ const path = getPath(e.target, '');
30216
30411
  // console.log('>>>> _logDetails', path);
30217
30412
  const cut = path.substring(path.indexOf('/fx-fore'), path.length);
30218
30413
  const xpath = `/${cut}`;