diff --git a/README.md b/README.md
index 298458929..4686049ad 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,294 @@ Here are some of our high-level priorities to get to a production-ready state:
- Implement all types and appearances defined in [XLSForm](https://xlsform.org/en/ref-table/)
- Define a thoughtful interface for host applications that balances ease of use and flexibility
+Here is the feature matrix and the progress we have made so far:
+
+
+
+
+
+
+ ${\mathtt{Question \space \space types \space \space (basic \space \space functionality)\color{transparent}==== \color{green}███\color{LightGray}█████████████████ \space \color{initial} 15\%}}$
+
+
+
+| Feature | Progress |
+| -------------------------- | :------: |
+| text | ✅ |
+| integer | |
+| decimal | |
+| note | 🚧 |
+| select_one | ✅ |
+| select_multiple | ✅ |
+| repeat | ✅ |
+| group | ✅ |
+| geopoint | |
+| geotrace | |
+| geoshape | |
+| start-geopoint | |
+| range | |
+| image | |
+| barcode | |
+| audio | |
+| background-audio | |
+| video | |
+| file | |
+| date | |
+| time | |
+| datetime | |
+| rank | |
+| csv-external | |
+| acknowledge | |
+| start | |
+| end | |
+| today | |
+| deviceid | |
+| username | |
+| phonenumber | |
+| email | |
+| audit | |
+
+
+
+
+ ${\mathtt{Appearances\color{transparent}============================= \color{green}████\color{LightGray}████████████████ \space \color{initial} 21\%}}$
+
+
+
+| Feature | Progress |
+| -------------------------- | :------: |
+| numbers | |
+| multiline | |
+| url | |
+| ex: | |
+| thousands-sep | |
+| bearing | |
+| vertical | |
+| no-ticks | |
+| picker | |
+| rating | |
+| new | |
+| new-front | |
+| draw | |
+| annotate | |
+| signature | |
+| no-calendar | |
+| month-year | |
+| year | |
+| ethiopian | |
+| coptic | |
+| islamic | |
+| bikram-sambat | |
+| myanmar | |
+| persian | |
+| placement-map | |
+| maps | |
+| hide-input | |
+| minimal | ✅ |
+| search / autocomplete | ✅ |
+| quick | |
+| columns-pack | |
+| columns | ✅ |
+| columns-n | ✅ |
+| no-buttons | ✅ |
+| image-map | |
+| likert | |
+| map | |
+| field-list | ✅ |
+| label | ✅ |
+| list-nolabel | ✅ |
+| list | ✅ |
+| table-list | |
+
+
+
+
+ ${\mathtt{Parameters\color{transparent}============================== \color{green}████\color{LightGray}████████████████ \space \color{initial} 22\%}}$
+
+
+
+| Feature | Progress |
+| -------------------------------------------------------------------------------------------------------------------------------- | :------: |
+| randomize | ✅ |
+| seed | ✅ |
+| value | |
+| label | |
+| geopoint capture-accuracy, warning-accur
acy, allow-mock-accuracy | |
+| range start, end, step | |
+| image max-pixels | |
+| audio quality | |
+| Audit: location-priority, location-min-i
nterval, location-max-age, track-changes
, track-changes-reasons, identify-user | |
+
+
+
+
+ ${\mathtt{Form \space \space Logic\color{transparent}============================== \color{green}██████████\color{LightGray}██████████ \space \color{initial} 50\%}}$
+
+
+
+| Feature | Progress |
+| -------------------------- | :------: |
+| calculate | ✅ |
+| relevant | ✅ |
+| required | ✅ |
+| required message | 🚧 |
+| custom constraint | 🚧 |
+| constraint message | 🚧 |
+| read only | ✅ |
+| trigger | |
+| choice filter | ✅ |
+| default | ✅ |
+| query parameter | |
+| repeat_count | |
+
+
+
+
+ ${\mathtt{Descriptions \space \space and \space \space Annotations\color{transparent}============ \color{green}██\color{LightGray}██████████████████ \space \color{initial} 13\%}}$
+
+
+
+| Feature | Progress |
+| ---------------------------------------------- | :------: |
+| label | ✅ |
+| hint | |
+| guidance hint | |
+| Translations | ✅ |
+| Translations with field/question value | |
+| Markdown | |
+| Inline HTML | |
+| Form attachments | |
+| image | |
+| big-image | |
+| audio | |
+| video | |
+| secondary instance (external choice file
) | |
+| secondary instance (last saved) | |
+| autoplay | |
+
+
+
+
+ ${\mathtt{Theme \space \space and \space \space Layouts\color{transparent}======================= \color{green}█\color{LightGray}███████████████████ \space \color{initial} 9\%}}$
+
+
+
+| Feature | Progress |
+| -------------------------- | :------: |
+| grid | |
+| pages | |
+| print | |
+| logo | |
+| theme color | |
+| Submissions | |
+| preview | ✅ |
+| send | |
+| view | |
+| edit | |
+| attachments | |
+
+
+
+
+ ${\mathtt{Offline \space \space capabilities\color{transparent}==================== \color{green}█\color{LightGray}███████████████████ \space \color{initial} 0\%}}$
+
+
+
+| Feature | Progress |
+| ---------------------------- | :------: |
+| List of projects & forms | |
+| local persistence (single) | |
+| save as draft | |
+| offline entities | |
+| MBtiles / offline map layers | |
+
+
+
+
+ ${\mathtt{XPath\color{transparent}=================================== \color{green}██████████████████\color{LightGray}██ \space \color{initial} 94\%}}$
+
+
+
+| Feature | Progress |
+| --------------------------------------------------------------------------------------------------------------- | :------: |
+| operators | ✅ |
+| predicates | ✅ |
+| axes | ✅ |
+| string(\* arg) | ✅ |
+| concat(string arg*\|node-set arg*) | ✅ |
+| join(string separator, node-set nodes\*) | ✅ |
+| substr(string value, number start, numbe
r end?) | ✅ |
+| substring-before(string, string) | ✅ |
+| substring-after(string, string) | ✅ |
+| translate(string, string, string) | ✅ |
+| string-length(string arg) | ✅ |
+| normalize-space(string arg?) | ✅ |
+| contains(string haystack, string needle) | ✅ |
+| starts-with(string haystack, string need
le) | ✅ |
+| ends-with(string haystack, string needle
) | ✅ |
+| uuid(number?) | ✅ |
+| digest(string src, string algorithm, str
ing encoding?) | ✅ |
+| pulldata(string instance_id, string desi
red_element, string query_element, strin
g query) | |
+| if(boolean condition, _ then, _ else) | ✅ |
+| coalesce(string arg1, string arg2) | ✅ |
+| once(string calc) | ✅ |
+| true() | ✅ |
+| false() | ✅ |
+| boolean(\* arg) | ✅ |
+| boolean-from-string(string arg) | ✅ |
+| not(boolean arg) | ✅ |
+| regex(string value, string expression) | ✅ |
+| checklist(number min, number max, string
v\*) | ✅ |
+| weighted-checklist(number min, number ma
x, [string v, string w]\*) | ✅ |
+| number(\* arg) | ✅ |
+| random() | ✅ |
+| int(number arg) | ✅ |
+| sum(node-set arg) | ✅ |
+| max(node-set arg\*) | ✅ |
+| min(node-set arg\*) | ✅ |
+| round(number arg, number decimals?) | ✅ |
+| pow(number value, number power) | ✅ |
+| log(number arg) | ✅ |
+| log10(number arg) | ✅ |
+| abs(number arg) | ✅ |
+| sin(number arg) | ✅ |
+| cos(number arg) | ✅ |
+| tan(number arg) | ✅ |
+| asin(number arg) | ✅ |
+| acos(number arg) | ✅ |
+| atan(number arg) | ✅ |
+| atan2(number arg, number arg) | ✅ |
+| sqrt(number arg) | ✅ |
+| exp(number arg) | ✅ |
+| exp10(number arg) | ✅ |
+| pi() | ✅ |
+| count(node-set arg) | ✅ |
+| count-non-empty(node-set arg) | ✅ |
+| position(node arg?) | ✅ |
+| instance(string id) | ✅ |
+| current() | ✅ |
+| randomize(node-set arg, number seed) | ✅ |
+| today() | ✅ |
+| now() | ✅ |
+| format-date(date value, string format) | ✅ |
+| format-date-time(dateTime value, string
format) | ✅ |
+| date(\* value) | ✅ |
+| decimal-date-time(dateTime value) | ✅ |
+| decimal-time(time value) | ✅ |
+| selected(string list, string value) | ✅ |
+| selected-at(string list, number index) | ✅ |
+| count-selected(node node) | ✅ |
+| jr:choice-name(node node, string value) | |
+| jr:itext(string id) | ✅ |
+| indexed-repeat(node-set arg, node-set re
peat1, number index1, [node-set repeatN,
number indexN]{0,2}) | |
+| area(node-set ns\|geoshape gs) | ✅ |
+| distance(node-set ns\|geoshape gs\|geotr
ace gt\|(geopoint\|string) arg\*) | ✅ |
+| base64-decode(base64Binary input) | |
+
+
+
+
+
We welcome discussion about the project [on the ODK forum](https://forum.getodk.org/)! The forum is generally the preferred place for questions, issue reports, and feature requests unless you have information to add to an existing issue.
## Q&A
diff --git a/feature-matrix.json b/feature-matrix.json
new file mode 100644
index 000000000..26c65322a
--- /dev/null
+++ b/feature-matrix.json
@@ -0,0 +1,218 @@
+{
+ "Question types (basic functionality)": {
+ "text": "✅",
+ "integer": "",
+ "decimal": "",
+ "note": "🚧",
+ "select_one": "✅",
+ "select_multiple": "✅",
+ "repeat": "✅",
+ "group": "✅",
+ "geopoint": "",
+ "geotrace": "",
+ "geoshape": "",
+ "start-geopoint": "",
+ "range": "",
+ "image": "",
+ "barcode": "",
+ "audio": "",
+ "background-audio": "",
+ "video": "",
+ "file": "",
+ "date": "",
+ "time": "",
+ "datetime": "",
+ "rank": "",
+ "csv-external": "",
+ "acknowledge": "",
+ "start": "",
+ "end": "",
+ "today": "",
+ "deviceid": "",
+ "username": "",
+ "phonenumber": "",
+ "email": "",
+ "audit": ""
+ },
+ "Appearances": {
+ "numbers": "",
+ "multiline": "",
+ "url": "",
+ "ex:": "",
+ "thousands-sep": "",
+ "bearing": "",
+ "vertical": "",
+ "no-ticks": "",
+ "picker": "",
+ "rating": "",
+ "new": "",
+ "new-front": "",
+ "draw": "",
+ "annotate": "",
+ "signature": "",
+ "no-calendar": "",
+ "month-year": "",
+ "year": "",
+ "ethiopian": "",
+ "coptic": "",
+ "islamic": "",
+ "bikram-sambat": "",
+ "myanmar": "",
+ "persian": "",
+ "placement-map": "",
+ "maps": "",
+ "hide-input": "",
+ "minimal": "✅",
+ "search / autocomplete": "✅",
+ "quick": "",
+ "columns-pack": "",
+ "columns": "✅",
+ "columns-n": "✅",
+ "no-buttons": "✅",
+ "image-map": "",
+ "likert": "",
+ "map": "",
+ "field-list": "✅",
+ "label": "✅",
+ "list-nolabel": "✅",
+ "list": "✅",
+ "table-list": ""
+ },
+ "Parameters": {
+ "randomize": "✅",
+ "seed": "✅",
+ "value": "",
+ "label": "",
+ "geopoint capture-accuracy, warning-accuracy, allow-mock-accuracy": "",
+ "range start, end, step": "",
+ "image max-pixels": "",
+ "audio quality": "",
+ "Audit: location-priority, location-min-interval, location-max-age, track-changes, track-changes-reasons, identify-user": ""
+ },
+ "Form Logic": {
+ "calculate": "✅",
+ "relevant": "✅",
+ "required": "✅",
+ "required message": "🚧",
+ "custom constraint": "🚧",
+ "constraint message": "🚧",
+ "read only": "✅",
+ "trigger": "",
+ "choice filter": "✅",
+ "default": "✅",
+ "query parameter": "",
+ "repeat_count": ""
+ },
+ "Descriptions and Annotations": {
+ "label": "✅",
+ "hint": "",
+ "guidance hint": "",
+ "Translations": "✅",
+ "Translations with field/question value": "",
+ "Markdown": "",
+ "Inline HTML": "",
+ "Form attachments": "",
+ "image": "",
+ "big-image": "",
+ "audio": "",
+ "video": "",
+ "secondary instance (external choice file)": "",
+ "secondary instance (last saved)": "",
+ "autoplay": ""
+ },
+ "Theme and Layouts": {
+ "grid": "",
+ "pages": "",
+ "print": "",
+ "logo": "",
+ "theme color": "",
+ "Submissions": "",
+ "preview": "✅",
+ "send": "",
+ "view": "",
+ "edit": "",
+ "attachments": ""
+ },
+ "Offline capabilities": {
+ "List of projects & forms": "",
+ "local persistence (single)": "",
+ "save as draft": "",
+ "offline entities": "",
+ "MBtiles / offline map layers": ""
+ },
+ "XPath": {
+ "operators": "✅",
+ "predicates": "✅",
+ "axes": "✅",
+ "string(* arg)": "✅",
+ "concat(string arg*|node-set arg*)": "✅",
+ "join(string separator, node-set nodes*)": "✅",
+ "substr(string value, number start, number end?)": "✅",
+ "substring-before(string, string)": "✅",
+ "substring-after(string, string)": "✅",
+ "translate(string, string, string)": "✅",
+ "string-length(string arg)": "✅",
+ "normalize-space(string arg?)": "✅",
+ "contains(string haystack, string needle)": "✅",
+ "starts-with(string haystack, string needle)": "✅",
+ "ends-with(string haystack, string needle)": "✅",
+ "uuid(number?)": "✅",
+ "digest(string src, string algorithm, string encoding?)": "✅",
+ "pulldata(string instance_id, string desired_element, string query_element, string query)": "",
+ "if(boolean condition, * then, * else)": "✅",
+ "coalesce(string arg1, string arg2)": "✅",
+ "once(string calc)": "✅",
+ "true()": "✅",
+ "false()": "✅",
+ "boolean(* arg)": "✅",
+ "boolean-from-string(string arg)": "✅",
+ "not(boolean arg)": "✅",
+ "regex(string value, string expression)": "✅",
+ "checklist(number min, number max, string v*)": "✅",
+ "weighted-checklist(number min, number max, [string v, string w]*)": "✅",
+ "number(* arg)": "✅",
+ "random()": "✅",
+ "int(number arg)": "✅",
+ "sum(node-set arg)": "✅",
+ "max(node-set arg*)": "✅",
+ "min(node-set arg*)": "✅",
+ "round(number arg, number decimals?)": "✅",
+ "pow(number value, number power)": "✅",
+ "log(number arg)": "✅",
+ "log10(number arg)": "✅",
+ "abs(number arg)": "✅",
+ "sin(number arg)": "✅",
+ "cos(number arg)": "✅",
+ "tan(number arg)": "✅",
+ "asin(number arg)": "✅",
+ "acos(number arg)": "✅",
+ "atan(number arg)": "✅",
+ "atan2(number arg, number arg)": "✅",
+ "sqrt(number arg)": "✅",
+ "exp(number arg)": "✅",
+ "exp10(number arg)": "✅",
+ "pi()": "✅",
+ "count(node-set arg)": "✅",
+ "count-non-empty(node-set arg)": "✅",
+ "position(node arg?)": "✅",
+ "instance(string id)": "✅",
+ "current()": "✅",
+ "randomize(node-set arg, number seed)": "✅",
+ "today()": "✅",
+ "now()": "✅",
+ "format-date(date value, string format)": "✅",
+ "format-date-time(dateTime value, string format)": "✅",
+ "date(* value)": "✅",
+ "decimal-date-time(dateTime value)": "✅",
+ "decimal-time(time value)": "✅",
+ "selected(string list, string value)": "✅",
+ "selected-at(string list, number index)": "✅",
+ "count-selected(node node)": "✅",
+ "jr:choice-name(node node, string value)": "",
+ "jr:itext(string id)": "✅",
+ "indexed-repeat(node-set arg, node-set repeat1, number index1, [node-set repeatN, number indexN]{0,2})": " ",
+ "area(node-set ns|geoshape gs)": "✅",
+ "distance(node-set ns|geoshape gs|geotrace gt|(geopoint|string) arg*)": "✅",
+ "base64-decode(base64Binary input)": ""
+ }
+}
diff --git a/package.json b/package.json
index 80208aa81..eb3b6e9c7 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,8 @@
"bump": "changeset add",
"format": "prettier -w \"**/*\" --ignore-unknown --cache",
"format:checkonly": "prettier -c \"**/*\" --ignore-unknown",
- "lint": "eslint . --report-unused-disable-directives"
+ "lint": "eslint . --report-unused-disable-directives",
+ "feature-matrix": "node scripts/feature-matrix/render.js"
},
"dependencies": {
"@changesets/changelog-github": "^0.5.0",
diff --git a/scripts/feature-matrix/render.js b/scripts/feature-matrix/render.js
new file mode 100644
index 000000000..46f8b2a38
--- /dev/null
+++ b/scripts/feature-matrix/render.js
@@ -0,0 +1,75 @@
+/* eslint-disable */
+// @ts-nocheck
+
+import Mustache from 'mustache';
+import fs from 'node:fs/promises';
+
+const rootUrl = new URL('../..', import.meta.url);
+const featureMatrix = JSON.parse(
+ await fs.readFile(new URL('./feature-matrix.json', rootUrl), 'utf-8')
+);
+const template = await fs.readFile(
+ new URL('scripts/feature-matrix/template.mustache', rootUrl),
+ 'utf-8'
+);
+const readmeFile = await fs.readFile(new URL('./README.md', rootUrl), 'utf-8');
+
+// Modified version of https://gist.github.com/rougier/c0d31f5cbdaac27b876c?permalink_comment_id=2269298#gistcomment-2269298
+const progress = ({ value, length = 20 }) => {
+ const v = (value / 100) * length;
+ const x = v < 1 ? 1 : Math.floor(v);
+ const bar = Array(x).fill('█').join('');
+ const remaining = Array(length - bar.length)
+ .fill('█')
+ .join('');
+ return `\\color{green}${bar}\\color{LightGray}${remaining} \\space \\color{initial} ${value}\\%`;
+};
+
+// Not so smart, blindly breaks the word. Okay for now.
+const wrapString = (str, maxLength) => {
+ let parts = [];
+ for (let i = 0; i < str.length; i += maxLength) {
+ parts.push(str.slice(i, i + maxLength));
+ }
+ return parts.join('
');
+};
+
+// Transform feature-matrix.json object into array
+const featureCategories = Object.keys(featureMatrix).map((featureCategory) => {
+ const features = Object.keys(featureMatrix[featureCategory]).map((feature) => {
+ return {
+ label: wrapString(feature.replaceAll('|', '\\|'), 40),
+ status: featureMatrix[featureCategory][feature],
+ };
+ });
+
+ // no points for 🚧
+ const progressPercentage = Math.floor(
+ (features.filter((f) => f.status === '✅').length / features.length) * 100
+ );
+
+ // hack: Space character in Latex monospace font is not same size as other characters.
+ // Using '=' sign with transparent color for padding. 🙃
+ let label = `${featureCategory}`.padEnd(40, '=').replaceAll(' ', ' \\space \\space ');
+ label = label.replace('=', '\\color{transparent}=');
+
+ const progressOutput = progress({ value: progressPercentage });
+
+ return {
+ // Using Latex for of collapsible - this allows using colored text in MD.
+ // mathtt is for monospace.
+ label: '${\\mathtt{' + label + ' ' + progressOutput + '}}$',
+ features,
+ };
+});
+
+const featureMatrixMd = Mustache.render(template, { categories: featureCategories });
+
+const autogenOpen = '';
+const autogenClose = '';
+
+const regex = new RegExp(`(${autogenOpen})[\\s\\S]*?(${autogenClose})`, 'm');
+
+const updatedReadme = readmeFile.replace(regex, `$1\n${featureMatrixMd}\n$2`);
+
+await fs.writeFile(new URL('./README.md', rootUrl), updatedReadme, { encoding: 'utf-8' });
diff --git a/scripts/feature-matrix/template.mustache b/scripts/feature-matrix/template.mustache
new file mode 100644
index 000000000..9ba089b2d
--- /dev/null
+++ b/scripts/feature-matrix/template.mustache
@@ -0,0 +1,15 @@
+{{ #categories }}
+
+
+ {{{ label }}}
+
+
+
+ | Feature | Progress |
+ | ----------- | :----------: |
+ {{ #features }}
+ | {{{ label }}} | {{ status }} |
+ {{ /features }}
+
+
+{{ /categories }}
\ No newline at end of file
diff --git a/scripts/package.json b/scripts/package.json
index 7142d695b..d3e73d9b9 100644
--- a/scripts/package.json
+++ b/scripts/package.json
@@ -7,6 +7,7 @@
"test": "true"
},
"devDependencies": {
- "globby": "^14.0.2"
+ "globby": "^14.0.2",
+ "mustache": "4.2.0"
}
}
diff --git a/yarn.lock b/yarn.lock
index f8c479806..cc0c05c51 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4804,6 +4804,11 @@ muggle-string@^0.4.0:
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
+mustache@4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+
mute-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e"