@getodk/xforms-engine 0.13.0 → 0.14.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/client/BaseItem.d.ts +6 -0
- package/dist/client/GroupNode.d.ts +4 -4
- package/dist/client/MarkdownNode.d.ts +33 -0
- package/dist/client/RankNode.d.ts +2 -4
- package/dist/client/SelectNode.d.ts +2 -5
- package/dist/client/TextRange.d.ts +2 -2
- package/dist/client/hierarchy.d.ts +1 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/node-types.d.ts +1 -1
- package/dist/index.js +10411 -327
- package/dist/index.js.map +1 -1
- package/dist/instance/hierarchy.d.ts +5 -6
- package/dist/instance/markdown/MarkdownNode.d.ts +75 -0
- package/dist/instance/text/TextChunk.d.ts +0 -1
- package/dist/instance/text/TextRange.d.ts +2 -1
- package/dist/instance/text/markdownFormat.d.ts +3 -0
- package/dist/lib/reactivity/createItemCollection.d.ts +5 -7
- package/dist/parse/body/BodyDefinition.d.ts +2 -8
- package/dist/parse/body/GroupElementDefinition.d.ts +22 -0
- package/dist/parse/body/control/ItemsetDefinition.d.ts +3 -0
- package/dist/parse/expression/ItemPropertyExpression.d.ts +6 -0
- package/dist/parse/model/{SubtreeDefinition.d.ts → GroupDefinition.d.ts} +4 -3
- package/dist/parse/model/NodeDefinition.d.ts +7 -8
- package/dist/parse/model/RepeatDefinition.d.ts +1 -1
- package/dist/parse/model/RootDefinition.d.ts +1 -1
- package/dist/parse/text/LabelDefinition.d.ts +4 -5
- package/dist/solid.js +10411 -327
- package/dist/solid.js.map +1 -1
- package/package.json +3 -2
- package/src/client/BaseItem.ts +7 -0
- package/src/client/GroupNode.ts +4 -10
- package/src/client/MarkdownNode.ts +53 -0
- package/src/client/RankNode.ts +2 -5
- package/src/client/SelectNode.ts +2 -6
- package/src/client/TextRange.ts +2 -2
- package/src/client/hierarchy.ts +0 -2
- package/src/client/index.ts +1 -1
- package/src/client/node-types.ts +0 -1
- package/src/instance/Group.ts +1 -1
- package/src/instance/children/buildChildren.ts +1 -17
- package/src/instance/children/normalizeChildInitOptions.ts +44 -59
- package/src/instance/hierarchy.ts +0 -6
- package/src/instance/markdown/MarkdownNode.ts +115 -0
- package/src/instance/text/TextChunk.ts +0 -5
- package/src/instance/text/TextRange.ts +5 -3
- package/src/instance/text/markdownFormat.ts +214 -0
- package/src/integration/xpath/adapter/names.ts +0 -1
- package/src/lib/reactivity/createItemCollection.ts +25 -9
- package/src/parse/body/BodyDefinition.ts +7 -34
- package/src/parse/body/GroupElementDefinition.ts +47 -0
- package/src/parse/body/control/ItemsetDefinition.ts +7 -0
- package/src/parse/expression/ItemPropertyExpression.ts +12 -0
- package/src/parse/model/{SubtreeDefinition.ts → GroupDefinition.ts} +6 -8
- package/src/parse/model/NodeDefinition.ts +8 -9
- package/src/parse/model/RepeatDefinition.ts +2 -2
- package/src/parse/model/RootDefinition.ts +2 -2
- package/src/parse/text/LabelDefinition.ts +4 -9
- package/dist/client/SubtreeNode.d.ts +0 -56
- package/dist/instance/Subtree.d.ts +0 -38
- package/dist/instance/text/FormattedTextStub.d.ts +0 -1
- package/dist/parse/body/group/BaseGroupDefinition.d.ts +0 -40
- package/dist/parse/body/group/LogicalGroupDefinition.d.ts +0 -6
- package/dist/parse/body/group/PresentationGroupDefinition.d.ts +0 -11
- package/dist/parse/body/group/StructuralGroupDefinition.d.ts +0 -6
- package/src/client/SubtreeNode.ts +0 -61
- package/src/instance/Subtree.ts +0 -102
- package/src/instance/text/FormattedTextStub.ts +0 -8
- package/src/parse/body/group/BaseGroupDefinition.ts +0 -89
- package/src/parse/body/group/LogicalGroupDefinition.ts +0 -11
- package/src/parse/body/group/PresentationGroupDefinition.ts +0 -28
- package/src/parse/body/group/StructuralGroupDefinition.ts +0 -11
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnchorMarkdownNode,
|
|
3
|
+
type ChildMarkdownNode as ClientChildMarkdownNode,
|
|
4
|
+
type HtmlMarkdownNode as ClientHtmlMarkdownNode,
|
|
5
|
+
type ParentMarkdownNode as ClientParentMarkdownNode,
|
|
6
|
+
type StyledMarkdownNode as ClientStyledMarkdownNode,
|
|
7
|
+
type ElementName,
|
|
8
|
+
type MarkdownNode,
|
|
9
|
+
type MarkdownProperty,
|
|
10
|
+
} from '../../client';
|
|
11
|
+
|
|
12
|
+
abstract class ParentMarkdownNode implements ClientParentMarkdownNode {
|
|
13
|
+
readonly children;
|
|
14
|
+
readonly role = 'parent';
|
|
15
|
+
abstract elementName: ElementName;
|
|
16
|
+
constructor(children: MarkdownNode[]) {
|
|
17
|
+
this.children = children;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Heading1 extends ParentMarkdownNode {
|
|
22
|
+
readonly elementName = 'h1';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Heading2 extends ParentMarkdownNode {
|
|
26
|
+
readonly elementName = 'h2';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Heading3 extends ParentMarkdownNode {
|
|
30
|
+
readonly elementName = 'h3';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Heading4 extends ParentMarkdownNode {
|
|
34
|
+
readonly elementName = 'h4';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Heading5 extends ParentMarkdownNode {
|
|
38
|
+
readonly elementName = 'h5';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Heading6 extends ParentMarkdownNode {
|
|
42
|
+
readonly elementName = 'h6';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class Strong extends ParentMarkdownNode {
|
|
46
|
+
readonly elementName = 'strong';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Underline extends ParentMarkdownNode {
|
|
50
|
+
readonly elementName = 'u';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class Emphasis extends ParentMarkdownNode {
|
|
54
|
+
readonly elementName = 'em';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class OrderedList extends ParentMarkdownNode {
|
|
58
|
+
readonly elementName = 'ol';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class UnorderedList extends ParentMarkdownNode {
|
|
62
|
+
readonly elementName = 'ul';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ListItem extends ParentMarkdownNode {
|
|
66
|
+
readonly elementName = 'li';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class Anchor extends ParentMarkdownNode implements AnchorMarkdownNode {
|
|
70
|
+
readonly elementName = 'a';
|
|
71
|
+
readonly url: string;
|
|
72
|
+
constructor(children: MarkdownNode[], url: string) {
|
|
73
|
+
super(children);
|
|
74
|
+
this.url = url;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
abstract class StyledMarkdownNode implements ClientParentMarkdownNode {
|
|
79
|
+
readonly children;
|
|
80
|
+
readonly role = 'parent';
|
|
81
|
+
abstract elementName: ElementName;
|
|
82
|
+
readonly properties: MarkdownProperty | undefined;
|
|
83
|
+
constructor(children: MarkdownNode[], properties: MarkdownProperty | undefined) {
|
|
84
|
+
this.children = children;
|
|
85
|
+
this.properties = properties;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class Paragraph extends StyledMarkdownNode implements ClientStyledMarkdownNode {
|
|
90
|
+
readonly elementName = 'p';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class Span extends StyledMarkdownNode implements ClientStyledMarkdownNode {
|
|
94
|
+
readonly elementName = 'span';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class Div extends StyledMarkdownNode implements ClientStyledMarkdownNode {
|
|
98
|
+
readonly elementName = 'div';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class ChildMarkdownNode implements ClientChildMarkdownNode {
|
|
102
|
+
readonly role = 'child';
|
|
103
|
+
readonly value: string;
|
|
104
|
+
constructor(value: string) {
|
|
105
|
+
this.value = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class Html implements ClientHtmlMarkdownNode {
|
|
110
|
+
readonly role = 'html';
|
|
111
|
+
readonly unsafeHtml: string;
|
|
112
|
+
constructor(unsafeHtml: string) {
|
|
113
|
+
this.unsafeHtml = unsafeHtml;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type { ActiveLanguage } from '../../client/FormLanguage.ts';
|
|
2
2
|
import type { TextChunk as ClientTextChunk, TextChunkSource } from '../../client/TextRange.ts';
|
|
3
3
|
import type { TranslationContext } from '../internal-api/TranslationContext.ts';
|
|
4
|
-
import { FormattedTextStub } from './FormattedTextStub.ts';
|
|
5
4
|
|
|
6
5
|
export class TextChunk implements ClientTextChunk {
|
|
7
|
-
get formatted() {
|
|
8
|
-
return FormattedTextStub;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
get language(): ActiveLanguage {
|
|
12
7
|
return this.context.getActiveLanguage();
|
|
13
8
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
|
|
2
|
+
|
|
3
|
+
import type { MarkdownNode } from '../../client/MarkdownNode.ts';
|
|
2
4
|
import type {
|
|
3
5
|
TextRange as ClientTextRange,
|
|
4
6
|
TextChunk,
|
|
5
7
|
TextOrigin,
|
|
6
8
|
TextRole,
|
|
7
9
|
} from '../../client/TextRange.ts';
|
|
8
|
-
import {
|
|
10
|
+
import { format } from './markdownFormat.ts';
|
|
9
11
|
|
|
10
12
|
export interface MediaSources {
|
|
11
13
|
image?: JRResourceURL;
|
|
@@ -20,8 +22,8 @@ export class TextRange<Role extends TextRole, Origin extends TextOrigin>
|
|
|
20
22
|
yield* this.chunks;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
get formatted() {
|
|
24
|
-
return
|
|
25
|
+
get formatted(): MarkdownNode[] {
|
|
26
|
+
return format(this.chunks);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
get asString(): string {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Heading, Literal, RootContent } from 'mdast';
|
|
2
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
3
|
+
import { type MarkdownNode, type StyleProperty } from '../../client';
|
|
4
|
+
import type { TextChunk } from '../../client/TextRange.ts';
|
|
5
|
+
import {
|
|
6
|
+
Anchor,
|
|
7
|
+
ChildMarkdownNode,
|
|
8
|
+
Div,
|
|
9
|
+
Emphasis,
|
|
10
|
+
Heading1,
|
|
11
|
+
Heading2,
|
|
12
|
+
Heading3,
|
|
13
|
+
Heading4,
|
|
14
|
+
Heading5,
|
|
15
|
+
Heading6,
|
|
16
|
+
Html,
|
|
17
|
+
ListItem,
|
|
18
|
+
OrderedList,
|
|
19
|
+
Paragraph,
|
|
20
|
+
Span,
|
|
21
|
+
Strong,
|
|
22
|
+
Underline,
|
|
23
|
+
UnorderedList,
|
|
24
|
+
} from '../markdown/MarkdownNode.ts';
|
|
25
|
+
|
|
26
|
+
const STYLE_PROPERTY_REGEX = /style\s*=\s*(?:'|")(.+)(?:'|")/i;
|
|
27
|
+
const HTML_TAG_MAP = {
|
|
28
|
+
span: Span,
|
|
29
|
+
div: Div,
|
|
30
|
+
p: Paragraph,
|
|
31
|
+
u: Underline,
|
|
32
|
+
i: Emphasis,
|
|
33
|
+
em: Emphasis,
|
|
34
|
+
b: Strong,
|
|
35
|
+
strong: Strong,
|
|
36
|
+
};
|
|
37
|
+
const SUPPORTED_HTML_TAGS = Object.entries(HTML_TAG_MAP).map(([tag, type]) => {
|
|
38
|
+
return {
|
|
39
|
+
openRegex: RegExp(`^\\s*<\\s*${tag}[\\s+>]`, 'i'),
|
|
40
|
+
closeRegex: RegExp(`<\\s*/${tag}\\s*>`, 'i'),
|
|
41
|
+
type,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let outputStrings: Map<string, string>;
|
|
46
|
+
|
|
47
|
+
function validateStyleProperty(name: string | undefined, value: string | undefined): boolean {
|
|
48
|
+
if (!name || !value) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!['color', 'font-family', 'text-align', 'font-size'].includes(name)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (name === 'text-align' && !['left', 'right', 'center'].includes(value)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseStyle(tag: string): StyleProperty | undefined {
|
|
61
|
+
const styleProperty = STYLE_PROPERTY_REGEX.exec(tag);
|
|
62
|
+
if (!styleProperty || styleProperty.length < 2) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const styleValue = styleProperty[1] ?? '';
|
|
66
|
+
const properties = styleValue.split(';');
|
|
67
|
+
const entries: string[][] = [];
|
|
68
|
+
properties.forEach((property) => {
|
|
69
|
+
const [name, value] = property.split(':').map((val) => val.trim());
|
|
70
|
+
if (validateStyleProperty(name, value)) {
|
|
71
|
+
entries.push([name!, value!]);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (!entries.length) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
return Object.fromEntries(entries) as StyleProperty;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mdastHeading(tree: Heading, children: MarkdownNode[]): MarkdownNode {
|
|
81
|
+
if (tree.depth === 1) {
|
|
82
|
+
return new Heading1(children);
|
|
83
|
+
}
|
|
84
|
+
if (tree.depth === 2) {
|
|
85
|
+
return new Heading2(children);
|
|
86
|
+
}
|
|
87
|
+
if (tree.depth === 3) {
|
|
88
|
+
return new Heading3(children);
|
|
89
|
+
}
|
|
90
|
+
if (tree.depth === 4) {
|
|
91
|
+
return new Heading4(children);
|
|
92
|
+
}
|
|
93
|
+
if (tree.depth === 5) {
|
|
94
|
+
return new Heading5(children);
|
|
95
|
+
}
|
|
96
|
+
return new Heading6(children);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mdastNodeToOdkMarkdown(tree: RootContent): MarkdownNode | undefined {
|
|
100
|
+
if (tree.type === 'html') {
|
|
101
|
+
return new Html(tree.value);
|
|
102
|
+
}
|
|
103
|
+
if (tree.type === 'text' || tree.type === 'inlineCode') {
|
|
104
|
+
const outputString = outputStrings.get(tree.value);
|
|
105
|
+
if (outputString) {
|
|
106
|
+
const children = toOdkMarkdown(outputString);
|
|
107
|
+
return new Span(children, undefined);
|
|
108
|
+
}
|
|
109
|
+
return new ChildMarkdownNode(tree.value);
|
|
110
|
+
}
|
|
111
|
+
if ('children' in tree) {
|
|
112
|
+
const children = mdastToOdkMarkdown(tree.children);
|
|
113
|
+
if (tree.type === 'paragraph') {
|
|
114
|
+
return new Paragraph(children, undefined);
|
|
115
|
+
}
|
|
116
|
+
if (tree.type === 'strong') {
|
|
117
|
+
return new Strong(children);
|
|
118
|
+
}
|
|
119
|
+
if (tree.type === 'emphasis') {
|
|
120
|
+
return new Emphasis(children);
|
|
121
|
+
}
|
|
122
|
+
if (tree.type === 'link') {
|
|
123
|
+
return new Anchor(children, tree.url);
|
|
124
|
+
}
|
|
125
|
+
if (tree.type === 'list') {
|
|
126
|
+
if (tree.ordered) {
|
|
127
|
+
return new OrderedList(children);
|
|
128
|
+
} else {
|
|
129
|
+
return new UnorderedList(children);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (tree.type === 'listItem') {
|
|
133
|
+
return new ListItem(children);
|
|
134
|
+
}
|
|
135
|
+
if (tree.type === 'heading') {
|
|
136
|
+
return mdastHeading(tree, children);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getUnclosedHtmlTag(tree: RootContent) {
|
|
143
|
+
if (tree.type !== 'html') {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const tag = SUPPORTED_HTML_TAGS.find((supported) => supported.openRegex.test(tree.value));
|
|
147
|
+
if (!tag || tag.closeRegex.test(tree.value)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
return tag;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mdastToOdkMarkdown(elements: RootContent[]): MarkdownNode[] {
|
|
154
|
+
const result: MarkdownNode[] = [];
|
|
155
|
+
for (let i = 0; i < elements.length; i++) {
|
|
156
|
+
const tree = elements[i]!;
|
|
157
|
+
const tag = getUnclosedHtmlTag(tree);
|
|
158
|
+
if (!tag) {
|
|
159
|
+
const odkMarkdown = mdastNodeToOdkMarkdown(tree);
|
|
160
|
+
if (odkMarkdown) {
|
|
161
|
+
result.push(odkMarkdown);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// SPECIAL CASE in mdast processing
|
|
167
|
+
// span children are parsed into siblings in the mdast for some reason
|
|
168
|
+
// so we need to advance `i` as we consume siblings
|
|
169
|
+
const children: RootContent[] = [];
|
|
170
|
+
let next = elements[++i];
|
|
171
|
+
while (next && !(next.type === 'html' && tag.closeRegex.test(next.value))) {
|
|
172
|
+
children.push(next);
|
|
173
|
+
next = elements[++i];
|
|
174
|
+
}
|
|
175
|
+
const odkChildren = mdastToOdkMarkdown(children);
|
|
176
|
+
const style = parseStyle((tree as Literal).value);
|
|
177
|
+
const properties = style && { style };
|
|
178
|
+
result.push(new tag.type(odkChildren, properties));
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function escapeEditableChunks(chunks: readonly TextChunk[]) {
|
|
184
|
+
return chunks
|
|
185
|
+
.map((chunk, i) => {
|
|
186
|
+
const str = chunk.asString;
|
|
187
|
+
if (str && chunk.source === 'output') {
|
|
188
|
+
// we need to process this separately otherwise user entered markup will
|
|
189
|
+
// interract with form markup in unexpected ways
|
|
190
|
+
const id = `--ODK-OUTPUT-STRING-${i}--`;
|
|
191
|
+
outputStrings.set(id, str);
|
|
192
|
+
return '`' + id + '`';
|
|
193
|
+
}
|
|
194
|
+
return str ?? '';
|
|
195
|
+
})
|
|
196
|
+
.join('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toOdkMarkdown(str: string): MarkdownNode[] {
|
|
200
|
+
const tree = fromMarkdown(str);
|
|
201
|
+
const odk = mdastToOdkMarkdown(tree.children);
|
|
202
|
+
if (odk.length === 1 && odk[0]?.role === 'parent' && odk[0]?.elementName === 'p') {
|
|
203
|
+
// mdast tends to add too many paragraphs which if left in place, puts a block level
|
|
204
|
+
// element where it's not needed
|
|
205
|
+
return odk[0].children;
|
|
206
|
+
}
|
|
207
|
+
return odk;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function format(chunks: readonly TextChunk[]): MarkdownNode[] {
|
|
211
|
+
outputStrings = new Map<string, string>();
|
|
212
|
+
const str = escapeEditableChunks(chunks);
|
|
213
|
+
return toOdkMarkdown(str);
|
|
214
|
+
}
|
|
@@ -140,7 +140,6 @@ export const resolveEngineXPathNodeNamespaceURI = (
|
|
|
140
140
|
case 'repeat-range:uncontrolled':
|
|
141
141
|
case 'root':
|
|
142
142
|
case 'select':
|
|
143
|
-
case 'subtree':
|
|
144
143
|
case 'trigger':
|
|
145
144
|
case 'upload':
|
|
146
145
|
return node.definition.namespaceDeclarations.get(prefix)?.declaredURI?.href ?? null;
|
|
@@ -2,13 +2,12 @@ import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
|
|
|
2
2
|
import type { Accessor } from 'solid-js';
|
|
3
3
|
import { createMemo } from 'solid-js';
|
|
4
4
|
import type { ActiveLanguage } from '../../client/FormLanguage.ts';
|
|
5
|
-
import type {
|
|
6
|
-
import type { RankItem } from '../../client/RankNode.ts';
|
|
5
|
+
import type { BaseItem } from '../../client/BaseItem.ts';
|
|
7
6
|
import type { TextRange as ClientTextRange } from '../../client/TextRange.ts';
|
|
8
7
|
import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts';
|
|
9
8
|
import type { TranslationContext } from '../../instance/internal-api/TranslationContext.ts';
|
|
10
|
-
import type { SelectControl } from '../../instance/SelectControl.ts';
|
|
11
9
|
import type { RankControl } from '../../instance/RankControl.ts';
|
|
10
|
+
import type { SelectControl } from '../../instance/SelectControl.ts';
|
|
12
11
|
import { TextChunk } from '../../instance/text/TextChunk.ts';
|
|
13
12
|
import { TextRange } from '../../instance/text/TextRange.ts';
|
|
14
13
|
import type { EngineXPathNode } from '../../integration/xpath/adapter/kind.ts';
|
|
@@ -19,8 +18,7 @@ import { createComputedExpression } from './createComputedExpression.ts';
|
|
|
19
18
|
import type { ReactiveScope } from './scope.ts';
|
|
20
19
|
import { createTextRange } from './text/createTextRange.ts';
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
type Item = RankItem | SelectItem;
|
|
21
|
+
type ItemCollectionControl = RankControl | SelectControl;
|
|
24
22
|
type DerivedItemLabel = ClientTextRange<'item-label', 'form-derived'>;
|
|
25
23
|
|
|
26
24
|
const derivedItemLabel = (context: TranslationContext, value: string): DerivedItemLabel => {
|
|
@@ -45,7 +43,7 @@ const createItemLabel = (
|
|
|
45
43
|
const createTranslatedStaticItems = (
|
|
46
44
|
control: ItemCollectionControl,
|
|
47
45
|
items: readonly ItemDefinition[]
|
|
48
|
-
): Accessor<readonly
|
|
46
|
+
): Accessor<readonly BaseItem[]> => {
|
|
49
47
|
return control.scope.runTask(() => {
|
|
50
48
|
const labeledItems = items.map((item) => {
|
|
51
49
|
const { value } = item;
|
|
@@ -54,6 +52,7 @@ const createTranslatedStaticItems = (
|
|
|
54
52
|
return () => ({
|
|
55
53
|
value,
|
|
56
54
|
label: label(),
|
|
55
|
+
properties: [],
|
|
57
56
|
});
|
|
58
57
|
});
|
|
59
58
|
|
|
@@ -101,6 +100,7 @@ const createItemsetItemLabel = (
|
|
|
101
100
|
interface ItemsetItem {
|
|
102
101
|
label(): ClientTextRange<'item-label'>;
|
|
103
102
|
value(): string;
|
|
103
|
+
properties: Array<[string, () => string]>;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const createItemsetItems = (
|
|
@@ -122,9 +122,20 @@ const createItemsetItems = (
|
|
|
122
122
|
});
|
|
123
123
|
const label = createItemsetItemLabel(context, itemset, value);
|
|
124
124
|
|
|
125
|
+
const nodeElements = itemNode
|
|
126
|
+
.getXPathChildNodes()
|
|
127
|
+
.filter((node) => node.nodeType === 'static-element');
|
|
128
|
+
const properties = itemset.getPropertiesExpressions(nodeElements).map((expression) => {
|
|
129
|
+
return [expression.toString(), createComputedExpression(context, expression)] as [
|
|
130
|
+
string,
|
|
131
|
+
() => string,
|
|
132
|
+
];
|
|
133
|
+
});
|
|
134
|
+
|
|
125
135
|
return {
|
|
126
136
|
label,
|
|
127
137
|
value,
|
|
138
|
+
properties,
|
|
128
139
|
};
|
|
129
140
|
});
|
|
130
141
|
});
|
|
@@ -135,7 +146,7 @@ const createItemsetItems = (
|
|
|
135
146
|
const createItemset = (
|
|
136
147
|
control: ItemCollectionControl,
|
|
137
148
|
itemset: ItemsetDefinition
|
|
138
|
-
): Accessor<readonly
|
|
149
|
+
): Accessor<readonly BaseItem[]> => {
|
|
139
150
|
return control.scope.runTask(() => {
|
|
140
151
|
const itemsetItems = createItemsetItems(control, itemset);
|
|
141
152
|
|
|
@@ -144,6 +155,9 @@ const createItemset = (
|
|
|
144
155
|
return {
|
|
145
156
|
label: item.label(),
|
|
146
157
|
value: item.value(),
|
|
158
|
+
properties: item.properties.map(
|
|
159
|
+
([propLabel, propValue]) => [propLabel, propValue()] as [string, string]
|
|
160
|
+
),
|
|
147
161
|
};
|
|
148
162
|
});
|
|
149
163
|
});
|
|
@@ -152,7 +166,7 @@ const createItemset = (
|
|
|
152
166
|
|
|
153
167
|
/**
|
|
154
168
|
* Creates a reactive computation of a {@link ItemCollectionControl}'s
|
|
155
|
-
* {@link
|
|
169
|
+
* {@link BaseItem}s, in support of the field's `valueOptions`.
|
|
156
170
|
*
|
|
157
171
|
* - The control defined with static `<item>`s will compute to an corresponding
|
|
158
172
|
* static list of items.
|
|
@@ -162,7 +176,9 @@ const createItemset = (
|
|
|
162
176
|
* their appropriate dependencies (whether relative to the itemset item node,
|
|
163
177
|
* referencing a form's `itext` translations, etc).
|
|
164
178
|
*/
|
|
165
|
-
export const createItemCollection = (
|
|
179
|
+
export const createItemCollection = (
|
|
180
|
+
control: ItemCollectionControl
|
|
181
|
+
): Accessor<readonly BaseItem[]> => {
|
|
166
182
|
const { items, itemset } = control.definition.bodyElement;
|
|
167
183
|
|
|
168
184
|
if (itemset != null) {
|
|
@@ -10,9 +10,7 @@ import type { AnySelectControlDefinition } from './control/SelectControlDefiniti
|
|
|
10
10
|
import { SelectControlDefinition } from './control/SelectControlDefinition.ts';
|
|
11
11
|
import { TriggerControlDefinition } from './control/TriggerControlDefinition.ts';
|
|
12
12
|
import { UploadControlDefinition } from './control/UploadControlDefinition.ts';
|
|
13
|
-
import {
|
|
14
|
-
import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts';
|
|
15
|
-
import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts';
|
|
13
|
+
import { GroupElementDefinition } from './GroupElementDefinition.ts';
|
|
16
14
|
import { RepeatElementDefinition } from './RepeatElementDefinition.ts';
|
|
17
15
|
import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts';
|
|
18
16
|
|
|
@@ -31,22 +29,18 @@ export type ControlElementDefinition =
|
|
|
31
29
|
| TriggerControlDefinition
|
|
32
30
|
| UploadControlDefinition;
|
|
33
31
|
|
|
32
|
+
// prettier-ignore
|
|
34
33
|
type SupportedBodyElementDefinition =
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| PresentationGroupDefinition
|
|
39
|
-
| StructuralGroupDefinition
|
|
40
|
-
| ControlElementDefinition;
|
|
34
|
+
| ControlElementDefinition
|
|
35
|
+
| GroupElementDefinition
|
|
36
|
+
| RepeatElementDefinition;
|
|
41
37
|
|
|
42
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
39
|
type BodyElementDefinitionConstructor = new (...args: any[]) => SupportedBodyElementDefinition;
|
|
44
40
|
|
|
45
41
|
const BodyElementDefinitionConstructors = [
|
|
46
42
|
RepeatElementDefinition,
|
|
47
|
-
|
|
48
|
-
PresentationGroupDefinition,
|
|
49
|
-
StructuralGroupDefinition,
|
|
43
|
+
GroupElementDefinition,
|
|
50
44
|
InputControlDefinition,
|
|
51
45
|
SelectControlDefinition,
|
|
52
46
|
RangeControlDefinition,
|
|
@@ -63,23 +57,6 @@ export type BodyElementDefinitionArray = readonly AnyBodyElementDefinition[];
|
|
|
63
57
|
|
|
64
58
|
export type AnyBodyElementType = AnyBodyElementDefinition['type'];
|
|
65
59
|
|
|
66
|
-
export type AnyGroupElementDefinition = Extract<
|
|
67
|
-
AnyBodyElementDefinition,
|
|
68
|
-
{ readonly type: `${string}-group` }
|
|
69
|
-
>;
|
|
70
|
-
|
|
71
|
-
const isGroupElementDefinition = (
|
|
72
|
-
element: AnyBodyElementDefinition
|
|
73
|
-
): element is AnyGroupElementDefinition => {
|
|
74
|
-
return element.type.endsWith('-group');
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const groupElementDefinition = (
|
|
78
|
-
element: AnyBodyElementDefinition
|
|
79
|
-
): AnyGroupElementDefinition | null => {
|
|
80
|
-
return isGroupElementDefinition(element) ? element : null;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
60
|
export type AnyControlElementDefinition = Extract<
|
|
84
61
|
AnyBodyElementDefinition,
|
|
85
62
|
{ readonly category: 'control' }
|
|
@@ -119,11 +96,7 @@ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition>
|
|
|
119
96
|
this.mapElementsByReference(element.children);
|
|
120
97
|
}
|
|
121
98
|
|
|
122
|
-
if (
|
|
123
|
-
element instanceof LogicalGroupDefinition ||
|
|
124
|
-
element instanceof PresentationGroupDefinition ||
|
|
125
|
-
element instanceof StructuralGroupDefinition
|
|
126
|
-
) {
|
|
99
|
+
if (element instanceof GroupElementDefinition) {
|
|
127
100
|
if (reference != null) {
|
|
128
101
|
this.set(reference, element);
|
|
129
102
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LabelDefinition } from '../text/LabelDefinition.ts';
|
|
2
|
+
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
3
|
+
import { parseNodesetReference } from '../xpath/reference-parsing.ts';
|
|
4
|
+
import type { StructureElementAppearanceDefinition } from './appearance/structureElementAppearanceParser.ts';
|
|
5
|
+
import { structureElementAppearanceParser } from './appearance/structureElementAppearanceParser.ts';
|
|
6
|
+
import {
|
|
7
|
+
type BodyElementDefinitionArray,
|
|
8
|
+
type BodyElementParentContext,
|
|
9
|
+
} from './BodyDefinition.ts';
|
|
10
|
+
import { BodyElementDefinition } from './BodyElementDefinition.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* As per the spec: https://getodk.github.io/xforms-spec/#groups
|
|
14
|
+
*
|
|
15
|
+
* A group combines elements together.
|
|
16
|
+
* The group can have a label, and if so is referred to as a "presentation group".
|
|
17
|
+
* The group can have a ref, and if so is referred to as a "logical group".
|
|
18
|
+
*/
|
|
19
|
+
export class GroupElementDefinition extends BodyElementDefinition<'group'> {
|
|
20
|
+
override readonly category = 'structure';
|
|
21
|
+
override readonly type = 'group';
|
|
22
|
+
|
|
23
|
+
readonly children: BodyElementDefinitionArray;
|
|
24
|
+
|
|
25
|
+
override readonly reference: string | null;
|
|
26
|
+
readonly appearances: StructureElementAppearanceDefinition;
|
|
27
|
+
override readonly label: LabelDefinition | null;
|
|
28
|
+
|
|
29
|
+
static override isCompatible(localName: string): boolean {
|
|
30
|
+
return localName === 'group';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
|
|
34
|
+
super(form, parent, element);
|
|
35
|
+
|
|
36
|
+
const childElements = Array.from(element.children).filter((child) => {
|
|
37
|
+
const childName = child.localName;
|
|
38
|
+
|
|
39
|
+
return childName !== 'label';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.children = this.body.getChildElementDefinitions(form, this, element, childElements);
|
|
43
|
+
this.reference = parseNodesetReference(parent, element, 'ref');
|
|
44
|
+
this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
|
|
45
|
+
this.label = LabelDefinition.forGroup(form, this);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { StaticElement } from '../../../integration/xpath/static-dom/StaticElement.ts';
|
|
1
2
|
import type { ItemsetElement } from '../../../lib/dom/query.ts';
|
|
2
3
|
import { getValueElement } from '../../../lib/dom/query.ts';
|
|
4
|
+
import { DependentExpression } from '../../expression/abstract/DependentExpression.ts';
|
|
5
|
+
import { ItemPropertyExpression } from '../../expression/ItemPropertyExpression.ts';
|
|
3
6
|
import { ItemsetNodesetExpression } from '../../expression/ItemsetNodesetExpression.ts';
|
|
4
7
|
import { ItemsetValueExpression } from '../../expression/ItemsetValueExpression.ts';
|
|
5
8
|
import { ItemsetLabelDefinition } from '../../text/ItemsetLabelDefinition.ts';
|
|
@@ -52,4 +55,8 @@ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> {
|
|
|
52
55
|
this.value = new ItemsetValueExpression(this, valueExpression);
|
|
53
56
|
this.label = ItemsetLabelDefinition.from(form, this);
|
|
54
57
|
}
|
|
58
|
+
|
|
59
|
+
getPropertiesExpressions(propertiesNodes: StaticElement[]): Array<DependentExpression<'string'>> {
|
|
60
|
+
return ItemPropertyExpression.from(propertiesNodes);
|
|
61
|
+
}
|
|
55
62
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
|
|
2
|
+
import { DependentExpression } from './abstract/DependentExpression.ts';
|
|
3
|
+
|
|
4
|
+
export class ItemPropertyExpression extends DependentExpression<'string'> {
|
|
5
|
+
static from(propertiesNodes: StaticElement[]) {
|
|
6
|
+
return propertiesNodes.map((node: StaticElement) => new this(node.qualifiedName.localName));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
constructor(propertyName: string) {
|
|
10
|
+
super('string', propertyName);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
|
|
2
2
|
import { NamespaceDeclarationMap } from '../../lib/names/NamespaceDeclarationMap.ts';
|
|
3
3
|
import { QualifiedName } from '../../lib/names/QualifiedName.ts';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
AnyGroupElementDefinition,
|
|
7
|
-
} from '../body/BodyDefinition.ts';
|
|
4
|
+
import type { AnyBodyElementDefinition } from '../body/BodyDefinition.ts';
|
|
5
|
+
import type { GroupElementDefinition } from '../body/GroupElementDefinition.ts';
|
|
8
6
|
import type { BindDefinition } from './BindDefinition.ts';
|
|
9
7
|
import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts';
|
|
10
8
|
import type { ChildNodeDefinition, ParentNodeDefinition } from './NodeDefinition.ts';
|
|
11
9
|
|
|
12
|
-
export class
|
|
13
|
-
'
|
|
14
|
-
|
|
10
|
+
export class GroupDefinition extends DescendentNodeDefinition<
|
|
11
|
+
'group',
|
|
12
|
+
GroupElementDefinition | null
|
|
15
13
|
> {
|
|
16
|
-
readonly type = '
|
|
14
|
+
readonly type = 'group';
|
|
17
15
|
|
|
18
16
|
readonly namespaceDeclarations: NamespaceDeclarationMap;
|
|
19
17
|
readonly qualifiedName: QualifiedName;
|