An XPath evaluator, with intent to support:
- Full XPath 1.0 syntax and behavior
- Context-based extensibility of behavior beyond the XPath 1.0 function library
- ODK XForms XPath extensions
- Non-browser environments (provided a subset of standard DOM APIs)
- Minor accommodations for certain Enketo function aliases:
date-time
is an alias ofdate
, which is assumed to be additiveformat-date
function is an alias offormat-date-time
, which differs from the ODK XForms spec
Install this package and its required peer dependencies with npm
(or the equivalent command for your preferred package manager):
npm install @getodk/tree-sitter-xpath @getodk/xpath web-tree-sitter
To use @getodk/xpath
at runtime, first create an XFormsXPathEvaluator
instance, specifying the XForm rootNode
. Usage from that point is
API-compatible with the standard DOM evaluate
method.
import { XFormsXPathEvaluator } from '@getodk/xpath';
// Given an XForms DOM document...
declare const xform: XMLDocument;
const evaluator = new XFormsXPathEvaluator({ rootNode: xform });
// A namespace resolver is optional, and the context node can be used (which is the default)
const nsResolver: XPathNSResolver = xform;
const result: XPathResult = evaluator.evaluate(
'/root/...',
xform,
nsResolver,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
For typical XForms usage, rootNode
will be either an XForm XMLDocument
or its primary instance Element
.
For XPath 1.0 functionality without XForms extension functions, you may use Evaluator
the same way, and rootNode
is optional:
import { Evaluator } from '@getodk/xpath';
const evaluator = new Evaluator();
// ...
In either case, the result
returned by evaluate
is API-compatible with the standard DOM XPathResult
.
XFormsXPathEvaluator
supports the JavaRosa itext
function (jr:itext
by convention), as specified in ODK XForms, which says:
Obtains an itext value for the provided reference in the active language from the
<itext>
block in the model.
This active language state is managed at the XFormXPathEvaluator
instance level, with the default language (again as specified in ODK XForms) active on construction. You can access a form's available languages, and get or set the active language under the XFormXPathEvaluator.translations
object.
Example:
const domParser = new DOMParser();
const xform: XMLDocument = domParser.parseFromString(
`<h:html>
<h:head>
<model>
<itext>
<translation lang="English" default="true()">
<text id="hello">
<value>hello</value>
</text>
</translation>
<translation lang="Español">
<text id="hello">
<value>hola</value>
</text>
</translation>
</itext>
</model>
</h:head>
<!-- ... -->
</h:html>`,
'text/xml'
);
const evaluator = new XFormsXPathEvaluator({ rootNode: xform });
evaluator.translations.getLanguages(); // ['English', 'Español']
evaluator.translations.getActiveLanguage(); // 'English'
evaluator.evaluate('jr:itext("hello")', xform, null, XPathResult.STRING_TYPE).stringValue; // 'hello'
evaluator.translations.setActiveLanguage('Español'); // 'Español'
evaluator.translations.getActiveLanguage(); // 'Español'
evaluator.evaluate('jr:itext("hello")', xform, null, XPathResult.STRING_TYPE).stringValue; // 'hola'
There are currently a few caveats to jr:itext
use:
-
<itext>
and its translations are evaluated from the document root of therootNode
specified inXFormsXPathEvaluator
options. As such:-
translations will be resolved if a descendant
rootNode
(e.g. the XForm's primary<instance>
element) is specified -
translations will not be resolved for an XForm in an unusual DOM structure (e.g. a
DocumentFragment
, or in an arbitrary subtree of an unrelated document)
-
-
Translations are treated as static, and cached during construction of
XFormsXPathEvaluator
. This is based on typical usage, wherein an XForm definition itself is expected to be static, but it will not (yet) support use cases like authoring an XForm definition. -
<value form="...anything...">
is not yet supported. It's unclear what the interface for this usage might be. -
The interface for getting and setting language state is currently experimental pending integration experience, and may be changed in the future. The intent of this interface is to be relatively agnostic to outside state management, and to isolate this sort of stateful context from the XForm DOM, but that approach may also change.
Both evaluator classes provide the following convenience methods:
evaluateBoolean(expression: string, options?: { contextNode?: Node }): boolean
evaluateNumber(expression: string, options?: { contextNode?: Node }): number
evaluateString(expression: string, options?: { contextNode?: Node }): string
evaluateNode(expression: string, options?: { contextNode?: Node }): Node | null
evaluateNodes(expression: string, options?: { contextNode?: Node }): Node[]
(Also provided: evaluateElement
and evaluateNonNullElement
, but these are not type safe so use them at your own risk.)
- Browsers (latest versions):
- Chrome/Chromium-based browsers (tested only in Chromium)
- Firefox
- Safari/WebKit (tested in WebKit directly)
- Non-browser runtimes with a DOM compatibility environement:
- Node (current/LTS; tested with jsdom). Node DOM compatibility does not require any native extensions for. DOM compatibility does not require any underlying XPath evaluation functionality (though it does currently rely on global constants like
XPathResult
). - Other runtimes and DOM compatibility libraries are not currently tested, support is unknown.
- Node (current/LTS; tested with jsdom). Node DOM compatibility does not require any native extensions for. DOM compatibility does not require any underlying XPath evaluation functionality (though it does currently rely on global constants like
- Expressions with variable references are parsed incorrectly. There is also no API to provide variable values, even if variable references were supported.
We intend to support the full ODK XForms function library, but support is currently incomplete. The following functions are not yet supported (the jr:
prefix is used by convention to refer to the JavaRosa namespace):
pulldata
jr:choice-name
The DOM standard is very complex, and compatibility libraries sometimes differ in terms of standards compliance and compatibility with the major browser implementations. It is likely, perhaps inevitable, that there will be edge cases and minor compatiblity issues when using such a library. At time of writing, no such issues are known in our tested environments, but a couple of minor issues have already been found and fixed during the initial test setup.