@qti-components/base 2.2.0 → 2.4.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/abstract/interaction.d.ts +1 -1
- package/dist/abstract/interaction.js +97 -5
- package/dist/abstract/interaction.js.map +1 -1
- package/dist/abstract/qti-expression.d.ts +26 -1
- package/dist/abstract/qti-expression.js +219 -99
- package/dist/abstract/qti-expression.js.map +1 -1
- package/dist/abstract/qti-feedback.d.ts +1 -2
- package/dist/abstract/qti-feedback.js +39 -12
- package/dist/abstract/qti-feedback.js.map +1 -1
- package/dist/context/context-root.d.ts +12 -0
- package/dist/context/context-root.js +24 -0
- package/dist/context/context-root.js.map +1 -0
- package/dist/context/session.context.d.ts +2 -0
- package/dist/context/session.context.js +1 -1
- package/dist/context/session.context.js.map +1 -1
- package/dist/context/types/item.types.d.ts +10 -0
- package/dist/context/types/item.types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/custom-operators.d.ts +31 -0
- package/dist/lib/custom-operators.js +63 -0
- package/dist/lib/custom-operators.js.map +1 -0
- package/dist/lib/qti-rule-base.d.ts +25 -0
- package/dist/lib/qti-rule-base.js +31 -0
- package/dist/lib/qti-rule-base.js.map +1 -1
- package/dist/lib/variables.d.ts +17 -1
- package/dist/lib/variables.js.map +1 -1
- package/dist/utils/scoring-helper.js +37 -4
- package/dist/utils/scoring-helper.js.map +1 -1
- package/dist/utils/utils.d.ts +10 -0
- package/dist/utils/utils.js +11 -11
- package/dist/utils/utils.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,33 +6,105 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { consume } from '@lit/context';
|
|
8
8
|
import { css, html, LitElement } from 'lit';
|
|
9
|
-
import { state } from 'lit/decorators.js';
|
|
10
9
|
import { itemContext } from '../context/item.context';
|
|
11
10
|
import { qtiContext } from '../context/qti.context';
|
|
12
11
|
import { testContext } from '../context/test.context';
|
|
12
|
+
/**
|
|
13
|
+
* Wrap whatever an expression handed back in a variable, reading the base type
|
|
14
|
+
* off the value itself.
|
|
15
|
+
*
|
|
16
|
+
* An operator that knows its own result type says so through `resultBaseType`
|
|
17
|
+
* and that wins, because the QTI vocabulary fixes it regardless of the operands
|
|
18
|
+
* — `qti-divide` is a float even when it divides two integers exactly. Only
|
|
19
|
+
* when nothing is declared, as for a custom operator returning whatever the
|
|
20
|
+
* host computed, is the type read off the value.
|
|
21
|
+
*
|
|
22
|
+
* Everything used to be labelled `integer`, and since `compareSingleValues`
|
|
23
|
+
* parses an integer with `parseInt`, that silently truncated:
|
|
24
|
+
* `equal(divide(5, 2), 2.4)` compared 2 against 2 and answered true.
|
|
25
|
+
*/
|
|
26
|
+
const variableFromValue = (value, declared) => {
|
|
27
|
+
/*
|
|
28
|
+
* A NULL result is still a value: the operand exists and is NULL. Returning
|
|
29
|
+
* nothing would drop it from the list, which both hides the NULL from
|
|
30
|
+
* `qti-is-null` and shifts every operand after it — a binary operator with a
|
|
31
|
+
* NULL first argument would silently read its second as its first. The base
|
|
32
|
+
* type is moot here, since every operator tests the value before it.
|
|
33
|
+
*/
|
|
34
|
+
if (value === null || value === undefined) {
|
|
35
|
+
return {
|
|
36
|
+
identifier: '',
|
|
37
|
+
baseType: 'string',
|
|
38
|
+
value: null,
|
|
39
|
+
cardinality: 'single',
|
|
40
|
+
type: 'response'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return {
|
|
45
|
+
identifier: '',
|
|
46
|
+
baseType: declared ?? inferBaseType(value[0]),
|
|
47
|
+
value: value.map(entry => String(entry)),
|
|
48
|
+
cardinality: 'multiple',
|
|
49
|
+
type: 'response'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
identifier: '',
|
|
54
|
+
baseType: declared ?? inferBaseType(value),
|
|
55
|
+
value: String(value),
|
|
56
|
+
cardinality: 'single',
|
|
57
|
+
type: 'response'
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** A JS value's QTI base type. A string stays text; it is not re-parsed. */
|
|
61
|
+
const inferBaseType = (value) => {
|
|
62
|
+
if (typeof value === 'number')
|
|
63
|
+
return Number.isInteger(value) ? 'integer' : 'float';
|
|
64
|
+
if (typeof value === 'boolean')
|
|
65
|
+
return 'boolean';
|
|
66
|
+
return 'string';
|
|
67
|
+
};
|
|
68
|
+
const inferContainerBaseType = (expression, value) => {
|
|
69
|
+
if (!Array.isArray(value) || value.length > 0) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return expression
|
|
73
|
+
.lastVariables
|
|
74
|
+
.find(variable => variable?.baseType && variable.baseType !== 'record' && variable.cardinality !== 'record')
|
|
75
|
+
?.baseType;
|
|
76
|
+
};
|
|
13
77
|
export class QtiExpression extends LitElement {
|
|
14
78
|
constructor() {
|
|
15
79
|
super(...arguments);
|
|
16
|
-
this
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
80
|
+
this.#lastVariables = [];
|
|
81
|
+
this.getVariables = () => {
|
|
82
|
+
// FIXME: if this itself is multiple, this will never enter the qti-multiple switch
|
|
83
|
+
// See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml
|
|
84
|
+
const variables = Array.from(this.children)
|
|
85
|
+
.map((e) => {
|
|
86
|
+
switch (e.tagName.toLowerCase()) {
|
|
87
|
+
case 'qti-base-value': {
|
|
88
|
+
return {
|
|
89
|
+
baseType: e.getAttribute('base-type'),
|
|
90
|
+
value: e.textContent.trim(),
|
|
91
|
+
cardinality: 'single'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case 'qti-variable': {
|
|
95
|
+
const identifier = e.getAttribute('identifier') || '';
|
|
96
|
+
// Special handling for QTI_CONTEXT
|
|
97
|
+
if (identifier === 'QTI_CONTEXT') {
|
|
98
|
+
if (!this.qtiContext?.QTI_CONTEXT) {
|
|
99
|
+
console.warn('QTI_CONTEXT not available');
|
|
100
|
+
return {
|
|
101
|
+
identifier: 'QTI_CONTEXT',
|
|
102
|
+
baseType: 'record',
|
|
103
|
+
value: this.qtiContext.QTI_CONTEXT,
|
|
104
|
+
cardinality: 'record',
|
|
105
|
+
type: 'context'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
36
108
|
return {
|
|
37
109
|
identifier: 'QTI_CONTEXT',
|
|
38
110
|
baseType: 'record',
|
|
@@ -41,82 +113,102 @@ export class QtiExpression extends LitElement {
|
|
|
41
113
|
type: 'context'
|
|
42
114
|
};
|
|
43
115
|
}
|
|
44
|
-
return
|
|
45
|
-
identifier: 'QTI_CONTEXT',
|
|
46
|
-
baseType: 'record',
|
|
47
|
-
value: this.qtiContext.QTI_CONTEXT,
|
|
48
|
-
cardinality: 'record',
|
|
49
|
-
type: 'context'
|
|
50
|
-
};
|
|
116
|
+
return this.resolveVariable(identifier);
|
|
51
117
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
118
|
+
case 'qti-multiple': {
|
|
119
|
+
const multiple = e;
|
|
120
|
+
const values = multiple.getResult();
|
|
121
|
+
if (values?.length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
identifier: '',
|
|
124
|
+
baseType: values[0].baseType,
|
|
125
|
+
value: values.map(v => v.value),
|
|
126
|
+
cardinality: 'multiple',
|
|
127
|
+
type: 'response'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
66
131
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
case 'qti-ordered': {
|
|
133
|
+
const multiple = e;
|
|
134
|
+
const values = multiple.getResult();
|
|
135
|
+
if (values?.length > 0) {
|
|
136
|
+
return {
|
|
137
|
+
identifier: '',
|
|
138
|
+
baseType: values[0].baseType,
|
|
139
|
+
value: values.map(v => v.value),
|
|
140
|
+
cardinality: 'ordered',
|
|
141
|
+
type: 'response'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
80
145
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
146
|
+
case 'qti-repeat': {
|
|
147
|
+
const repeat = e;
|
|
148
|
+
const values = repeat.getResult();
|
|
149
|
+
if (values?.length > 0) {
|
|
150
|
+
return values;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
88
153
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
case 'qti-custom-operator': {
|
|
155
|
+
// Not a QtiExpression — it has `calculate` but no `getResult`, so
|
|
156
|
+
// the default branch below throws on it and the surrounding
|
|
157
|
+
// expression is handed nothing.
|
|
158
|
+
return variableFromValue(e.calculate?.());
|
|
159
|
+
}
|
|
160
|
+
case 'qti-correct': {
|
|
161
|
+
const identifier = e.getAttribute('identifier') || '';
|
|
162
|
+
const responseVariable = this.context?.variables?.find(v => v.identifier === identifier);
|
|
163
|
+
// A `qti-correct` naming a response that is not declared — a typo,
|
|
164
|
+
// or an expression evaluated before the declaration registered —
|
|
165
|
+
// used to dereference the null this deliberately produced and take
|
|
166
|
+
// down the whole processing run. It is an unresolved variable, so
|
|
167
|
+
// it is NULL.
|
|
168
|
+
if (!responseVariable) {
|
|
169
|
+
console.warn(`qti-correct: no response declaration for "${identifier}"`);
|
|
170
|
+
return variableFromValue(null);
|
|
171
|
+
}
|
|
105
172
|
return {
|
|
106
|
-
baseType:
|
|
107
|
-
value:
|
|
108
|
-
cardinality:
|
|
173
|
+
baseType: responseVariable.baseType,
|
|
174
|
+
value: responseVariable.correctResponse,
|
|
175
|
+
cardinality: responseVariable.cardinality
|
|
109
176
|
};
|
|
110
177
|
}
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
default: {
|
|
179
|
+
// Every operator without a case of its own lands here.
|
|
180
|
+
try {
|
|
181
|
+
const expression = e;
|
|
182
|
+
const result = expression.getResult();
|
|
183
|
+
return variableFromValue(result, expression.resultBaseType ?? inferContainerBaseType(expression, result));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn(`getVariables: could not read a value from <${e.tagName.toLowerCase()}>`, error);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
113
189
|
}
|
|
114
|
-
return null;
|
|
115
190
|
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
191
|
+
})
|
|
192
|
+
.flatMap(v => (Array.isArray(v) ? v : [v]))
|
|
193
|
+
.filter((v) => v !== null);
|
|
194
|
+
this.#lastVariables = variables;
|
|
195
|
+
return variables;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
#lastVariables;
|
|
199
|
+
/**
|
|
200
|
+
* What the last `calculate()` produced, or undefined if it has not run.
|
|
201
|
+
*
|
|
202
|
+
* This is the supported way to inspect a scoring run: process the responses,
|
|
203
|
+
* then walk the rule tree reading this off each expression. A tool that wants
|
|
204
|
+
* to visualise how a score came about gets the whole tree annotated with its
|
|
205
|
+
* values, without the components having to re-render to show them.
|
|
206
|
+
*/
|
|
207
|
+
get lastResult() {
|
|
208
|
+
return this.result;
|
|
209
|
+
}
|
|
210
|
+
get lastVariables() {
|
|
211
|
+
return this.#lastVariables;
|
|
120
212
|
}
|
|
121
213
|
/*
|
|
122
214
|
* Written here rather than imported from a .css file, and that is a packaging constraint rather
|
|
@@ -145,9 +237,31 @@ export class QtiExpression extends LitElement {
|
|
|
145
237
|
display: none;
|
|
146
238
|
}
|
|
147
239
|
`; }
|
|
240
|
+
/*
|
|
241
|
+
* Just the slot. This used to render `JSON.stringify(this.result, null, 2)`
|
|
242
|
+
* into a <pre> beside it — a development aid that nobody could see, because
|
|
243
|
+
* expressions live inside `qti-response-processing`, which is `display: none`.
|
|
244
|
+
* It cost a full serialisation of the result on every render.
|
|
245
|
+
*/
|
|
148
246
|
render() {
|
|
149
|
-
return html `<
|
|
150
|
-
|
|
247
|
+
return html `<slot></slot>`;
|
|
248
|
+
}
|
|
249
|
+
/*
|
|
250
|
+
* Render once, then never again: the template above is constant, so every
|
|
251
|
+
* later update is pure overhead.
|
|
252
|
+
*
|
|
253
|
+
* It is not the `@state()` decorators that drive those updates — `@consume`
|
|
254
|
+
* subscribes through `ContextConsumer`, which calls `host.requestUpdate()`
|
|
255
|
+
* itself on every context change, "in case this value is used in a template".
|
|
256
|
+
* Here it is not. An item with 30 response-processing rules holds ~180
|
|
257
|
+
* expression elements and the test context changes on every keystroke, so
|
|
258
|
+
* that was ~180 renders per keystroke producing identical output.
|
|
259
|
+
*
|
|
260
|
+
* The subscriptions stay, because `calculate()` reads the current values when
|
|
261
|
+
* a rule invokes it. Only the rendering stops.
|
|
262
|
+
*/
|
|
263
|
+
shouldUpdate() {
|
|
264
|
+
return !this.hasUpdated;
|
|
151
265
|
}
|
|
152
266
|
calculate() {
|
|
153
267
|
this.result = this.getResult();
|
|
@@ -156,6 +270,18 @@ export class QtiExpression extends LitElement {
|
|
|
156
270
|
getResult() {
|
|
157
271
|
throw new Error('Not implemented');
|
|
158
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* The base type this operator always produces, when the QTI vocabulary fixes
|
|
275
|
+
* one regardless of its operands — `qti-divide` is a float even dividing two
|
|
276
|
+
* integers exactly, and `qti-round` is an integer whatever it rounds.
|
|
277
|
+
*
|
|
278
|
+
* Left undefined by operators whose result type follows their operands (a
|
|
279
|
+
* `qti-sum` of integers is an integer) and by those that return no number at
|
|
280
|
+
* all; the type is then read off the value.
|
|
281
|
+
*/
|
|
282
|
+
get resultBaseType() {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
159
285
|
/**
|
|
160
286
|
* Resolve a declared variable from the closest available scope: item-level
|
|
161
287
|
* first, then the test-level outcome variables. So an expression used in an
|
|
@@ -171,18 +297,12 @@ export class QtiExpression extends LitElement {
|
|
|
171
297
|
}
|
|
172
298
|
}
|
|
173
299
|
__decorate([
|
|
174
|
-
|
|
175
|
-
], QtiExpression.prototype, "result", void 0);
|
|
176
|
-
__decorate([
|
|
177
|
-
consume({ context: itemContext, subscribe: true }),
|
|
178
|
-
state()
|
|
300
|
+
consume({ context: itemContext, subscribe: true })
|
|
179
301
|
], QtiExpression.prototype, "context", void 0);
|
|
180
302
|
__decorate([
|
|
181
|
-
consume({ context: qtiContext, subscribe: true })
|
|
182
|
-
state()
|
|
303
|
+
consume({ context: qtiContext, subscribe: true })
|
|
183
304
|
], QtiExpression.prototype, "qtiContext", void 0);
|
|
184
305
|
__decorate([
|
|
185
|
-
consume({ context: testContext, subscribe: true })
|
|
186
|
-
state()
|
|
306
|
+
consume({ context: testContext, subscribe: true })
|
|
187
307
|
], QtiExpression.prototype, "_testContext", void 0);
|
|
188
308
|
//# sourceMappingURL=qti-expression.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qti-expression.js","sourceRoot":"","sources":["../../src/abstract/qti-expression.ts"],"names":[],"mappings":"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","sourcesContent":["import { consume } from '@lit/context';\nimport { css, html, LitElement } from 'lit';\nimport { state } from 'lit/decorators.js';\n\nimport { itemContext } from '../context/item.context';\nimport { qtiContext } from '../context/qti.context';\nimport { testContext } from '../context/test.context';\n\nimport type { ItemContext } from '../context/types/item.types';\nimport type { QtiContext, QtiContextType } from '../context/qti.context';\nimport type { ResponseVariable, VariableDeclaration } from '../lib/variables';\nimport type { TestContext } from '../context/test.context';\n\nexport interface QtiExpressionBase<T> {\n // get assessmentItem(): QtiAssessmentItem;\n\n // getVariables(): VariableDeclaration<number | string | (number | string)[] | null>[];\n calculate(): Readonly<T>;\n}\n\nexport abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {\n @state()\n protected result: any;\n\n /*\n * Written here rather than imported from a .css file, and that is a packaging constraint rather\n * than a style preference.\n *\n * The rule used to live in qti-expression.css, imported with Vite's inline-stylesheet query so\n * DevTools could show source-file provenance. That query is a VITE specifier, and tsc copies it\n * verbatim into dist — so the published package shipped an import only a Vite-based consumer\n * could resolve. Every other consumer broke on it: plain Node, tsx, any non-Vite bundler. It was\n * the single such specifier reachable from this package, and it took down QTI-Editor's schema\n * check, which loads the components under tsx to build the real ProseMirror schema\n * (ERR_UNKNOWN_FILE_EXTENSION on a file that has nothing to do with schemas).\n *\n * A `build:assets` step used to copy the .css into dist beside it. That went too — it existed\n * only to give the unresolvable import something to point at.\n *\n * Two lines of CSS are not worth a dist that only one bundler can consume. If this ever grows\n * into a real stylesheet, give it a `.styles.ts` exporting a `css` template — the house pattern\n * everywhere else in this repo — rather than reintroducing a bundler-specific import.\n *\n * The element renders a debug <pre> of its computed result plus a <slot>; hiding the slot is all\n * the styling it has ever had.\n */\n static override styles = css`\n slot {\n display: none;\n }\n `;\n\n override render() {\n return html`<pre>${JSON.stringify(this.result, null, 2)}</pre>\n <slot></slot>`;\n }\n\n public calculate(): Readonly<T> {\n this.result = this.getResult();\n return this.result;\n }\n\n protected getResult(): Readonly<T> {\n throw new Error('Not implemented');\n }\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n protected context?: ItemContext;\n\n @consume({ context: qtiContext, subscribe: true })\n @state()\n protected qtiContext?: QtiContext;\n\n @consume({ context: testContext, subscribe: true })\n @state()\n protected _testContext?: TestContext;\n\n /**\n * Resolve a declared variable from the closest available scope: item-level\n * first, then the test-level outcome variables. So an expression used in an\n * assessmentTest's outcome processing can reference a test-level outcome\n * (e.g. a total set earlier in the same run) — which the QTI variable\n * expression is specified to do. Returns null when unresolved rather than\n * throwing, so callers degrade gracefully.\n */\n protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null {\n return (\n this.context?.variables.find(v => v.identifier === identifier) ??\n this._testContext?.testOutcomeVariables?.find(v => v.identifier === identifier) ??\n null\n );\n }\n\n getVariables = (): (ResponseVariable | VariableDeclaration<QtiContextType>)[] =>\n // FIXME: if this itself is multiple, this will never enter the qti-multiple switch\n // See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml\n\n Array.from(this.children)\n .map((e: Element) => {\n console.debug('getVariables', e.tagName.toLowerCase());\n switch (e.tagName.toLowerCase()) {\n case 'qti-base-value': {\n return {\n baseType: e.getAttribute('base-type'),\n value: e.textContent.trim(),\n cardinality: 'single'\n } as ResponseVariable;\n }\n case 'qti-variable': {\n const identifier = e.getAttribute('identifier') || '';\n\n // Special handling for QTI_CONTEXT\n if (identifier === 'QTI_CONTEXT') {\n if (!this.qtiContext?.QTI_CONTEXT) {\n console.warn('QTI_CONTEXT not available');\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n\n return this.resolveVariable(identifier);\n }\n case 'qti-multiple': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n\n const values = multiple.getResult();\n console.debug('values', values);\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-ordered': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'ordered',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-repeat': {\n const repeat = e as QtiExpression<ResponseVariable[]>;\n const values = repeat.getResult();\n if (values?.length > 0) {\n return values;\n }\n return null;\n }\n case 'qti-correct': {\n const identifier = e.getAttribute('identifier') || '';\n const responseVariable: ResponseVariable =\n this.context?.variables?.find(v => v.identifier === identifier) || null;\n return {\n baseType: responseVariable.baseType,\n value: responseVariable.correctResponse,\n cardinality: responseVariable.cardinality\n } as ResponseVariable;\n }\n default: {\n // added for use of qti-equal-rounded\n try {\n const expression = e as QtiExpression<number>;\n const value = expression.getResult();\n return {\n baseType: 'integer',\n value: value?.toString() || null,\n cardinality: 'single'\n } as ResponseVariable;\n } catch (error) {\n console.warn('default not sufficient');\n }\n return null;\n }\n }\n })\n .flatMap(v => (Array.isArray(v) ? v : [v]))\n .filter(v => v !== null);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"qti-expression.js","sourceRoot":"","sources":["../../src/abstract/qti-expression.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAkBtD;;;;;;;;;;;;;GAaG;AACH,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAE,QAAmB,EAAoB,EAAE;IAClF;;;;;;OAMG;IACH,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO;YACL,UAAU,EAAE,EAAE;YACd,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,IAAI;YACX,WAAW,EAAE,QAAQ;YACrB,IAAI,EAAE,UAAU;SACG,CAAC;IACxB,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,UAAU,EAAE,EAAE;YACd,QAAQ,EAAE,QAAQ,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxC,WAAW,EAAE,UAAU;YACvB,IAAI,EAAE,UAAU;SACG,CAAC;IACxB,CAAC;IAED,OAAO;QACL,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,QAAQ,IAAI,aAAa,CAAC,KAAK,CAAC;QAC1C,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;QACpB,WAAW,EAAE,QAAQ;QACrB,IAAI,EAAE,UAAU;KACG,CAAC;AACxB,CAAC,CAAC;AAEF,4EAA4E;AAC5E,MAAM,aAAa,GAAG,CAAC,KAAc,EAAY,EAAE;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IACpF,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACjD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,sBAAsB,GAAG,CAAC,UAAkC,EAAE,KAAc,EAAwB,EAAE;IAC1G,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,UAAU;SACd,aAAa;SACb,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,CAAC;QAC5G,EAAE,QAAgC,CAAC;AACvC,CAAC,CAAC;AAEF,MAAM,OAAgB,aAAiB,SAAQ,UAAU;IAAzD;;QAOE,mBAAc,GAAyB,EAAE,CAAC;QAoI1C,iBAAY,GAAG,GAAyB,EAAE;YACxC,mFAAmF;YACnF,iJAAiJ;YAEjJ,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;iBACxC,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE;gBAClB,QAAQ,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;oBAChC,KAAK,gBAAgB,CAAC,CAAC,CAAC;wBACtB,OAAO;4BACL,QAAQ,EAAE,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC;4BACrC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE;4BAC3B,WAAW,EAAE,QAAQ;yBACF,CAAC;oBACxB,CAAC;oBACD,KAAK,cAAc,CAAC,CAAC,CAAC;wBACpB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;wBAEtD,mCAAmC;wBACnC,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;4BACjC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC;gCAClC,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;gCAC1C,OAAO;oCACL,UAAU,EAAE,aAAa;oCACzB,QAAQ,EAAE,QAAQ;oCAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;oCAClC,WAAW,EAAE,QAAQ;oCACrB,IAAI,EAAE,SAAS;iCACuB,CAAC;4BAC3C,CAAC;4BACD,OAAO;gCACL,UAAU,EAAE,aAAa;gCACzB,QAAQ,EAAE,QAAQ;gCAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;gCAClC,WAAW,EAAE,QAAQ;gCACrB,IAAI,EAAE,SAAS;6BACuB,CAAC;wBAC3C,CAAC;wBAED,OAAO,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;oBAC1C,CAAC;oBACD,KAAK,cAAc,CAAC,CAAC,CAAC;wBACpB,MAAM,QAAQ,GAAG,CAAsC,CAAC;wBAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;wBACpC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;4BACvB,OAAO;gCACL,UAAU,EAAE,EAAE;gCACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;gCAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;gCAC/B,WAAW,EAAE,UAAU;gCACvB,IAAI,EAAE,UAAU;6BACG,CAAC;wBACxB,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;oBACD,KAAK,aAAa,CAAC,CAAC,CAAC;wBACnB,MAAM,QAAQ,GAAG,CAAsC,CAAC;wBACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;wBACpC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;4BACvB,OAAO;gCACL,UAAU,EAAE,EAAE;gCACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;gCAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;gCAC/B,WAAW,EAAE,SAAS;gCACtB,IAAI,EAAE,UAAU;6BACG,CAAC;wBACxB,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;oBACD,KAAK,YAAY,CAAC,CAAC,CAAC;wBAClB,MAAM,MAAM,GAAG,CAAsC,CAAC;wBACtD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;wBAClC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;4BACvB,OAAO,MAAM,CAAC;wBAChB,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;oBACD,KAAK,qBAAqB,CAAC,CAAC,CAAC;wBAC3B,kEAAkE;wBAClE,4DAA4D;wBAC5D,gCAAgC;wBAChC,OAAO,iBAAiB,CAAE,CAA0B,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;oBACtE,CAAC;oBACD,KAAK,aAAa,CAAC,CAAC,CAAC;wBACnB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;wBACtD,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAE1E,CAAC;wBAEd,mEAAmE;wBACnE,iEAAiE;wBACjE,mEAAmE;wBACnE,kEAAkE;wBAClE,cAAc;wBACd,IAAI,CAAC,gBAAgB,EAAE,CAAC;4BACtB,OAAO,CAAC,IAAI,CAAC,6CAA6C,UAAU,GAAG,CAAC,CAAC;4BACzE,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;wBACjC,CAAC;wBAED,OAAO;4BACL,QAAQ,EAAE,gBAAgB,CAAC,QAAQ;4BACnC,KAAK,EAAE,gBAAgB,CAAC,eAAe;4BACvC,WAAW,EAAE,gBAAgB,CAAC,WAAW;yBACtB,CAAC;oBACxB,CAAC;oBACD,OAAO,CAAC,CAAC,CAAC;wBACR,uDAAuD;wBACvD,IAAI,CAAC;4BACH,MAAM,UAAU,GAAG,CAA2B,CAAC;4BAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;4BACtC,OAAO,iBAAiB,CAAC,MAAM,EAAE,UAAU,CAAC,cAAc,IAAI,sBAAsB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;wBAC5G,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BACf,OAAO,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;wBAChG,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;gBACH,CAAC;YACH,CAAC,CAAC;iBACD,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;iBAC1C,MAAM,CAAC,CAAC,CAAC,EAA2B,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YAEtD,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;YAChC,OAAO,SAAS,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC;IAhQC,cAAc,CAA4B;IAE1C;;;;;;;OAOG;IACH,IAAW,UAAU;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,IAAW,aAAa;QACtB,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;aACa,WAAM,GAAG,GAAG,CAAA;;;;GAI3B,AAJqB,CAIpB;IAEF;;;;;OAKG;IACM,MAAM;QACb,OAAO,IAAI,CAAA,eAAe,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;;;;OAaG;IACgB,YAAY;QAC7B,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;IAC1B,CAAC;IAEM,SAAS;QACd,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAES,SAAS;QACjB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;IAED;;;;;;;;OAQG;IACH,IAAW,cAAc;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAsBD;;;;;;;OAOG;IACO,eAAe,CAAC,UAAkB;QAC1C,OAAO,CACL,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;YAC9D,IAAI,CAAC,YAAY,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;YAC/E,IAAI,CACL,CAAC;IACJ,CAAC;;AAtBS;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;8CACnB;AAGtB;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;iDAChB;AAGxB;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;mDACd","sourcesContent":["import { consume } from '@lit/context';\nimport { css, html, LitElement } from 'lit';\n\nimport { itemContext } from '../context/item.context';\nimport { qtiContext } from '../context/qti.context';\nimport { testContext } from '../context/test.context';\n\nimport type { ItemContext } from '../context/types/item.types';\nimport type { QtiContext, QtiContextType } from '../context/qti.context';\nimport type { Calculate } from '../lib/expression-result';\nimport type { BaseType } from '../lib/expression-result';\nimport type { ResponseVariable, VariableDeclaration } from '../lib/variables';\nimport type { TestContext } from '../context/test.context';\n\ntype ExpressionVariable = ResponseVariable | VariableDeclaration<QtiContextType>;\n\nexport interface QtiExpressionBase<T> {\n // get assessmentItem(): QtiAssessmentItem;\n\n // getVariables(): VariableDeclaration<number | string | (number | string)[] | null>[];\n calculate(): Readonly<T>;\n}\n\n/**\n * Wrap whatever an expression handed back in a variable, reading the base type\n * off the value itself.\n *\n * An operator that knows its own result type says so through `resultBaseType`\n * and that wins, because the QTI vocabulary fixes it regardless of the operands\n * — `qti-divide` is a float even when it divides two integers exactly. Only\n * when nothing is declared, as for a custom operator returning whatever the\n * host computed, is the type read off the value.\n *\n * Everything used to be labelled `integer`, and since `compareSingleValues`\n * parses an integer with `parseInt`, that silently truncated:\n * `equal(divide(5, 2), 2.4)` compared 2 against 2 and answered true.\n */\nconst variableFromValue = (value: unknown, declared?: BaseType): ResponseVariable => {\n /*\n * A NULL result is still a value: the operand exists and is NULL. Returning\n * nothing would drop it from the list, which both hides the NULL from\n * `qti-is-null` and shifts every operand after it — a binary operator with a\n * NULL first argument would silently read its second as its first. The base\n * type is moot here, since every operator tests the value before it.\n */\n if (value === null || value === undefined) {\n return {\n identifier: '',\n baseType: 'string',\n value: null,\n cardinality: 'single',\n type: 'response'\n } as ResponseVariable;\n }\n\n if (Array.isArray(value)) {\n return {\n identifier: '',\n baseType: declared ?? inferBaseType(value[0]),\n value: value.map(entry => String(entry)),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n\n return {\n identifier: '',\n baseType: declared ?? inferBaseType(value),\n value: String(value),\n cardinality: 'single',\n type: 'response'\n } as ResponseVariable;\n};\n\n/** A JS value's QTI base type. A string stays text; it is not re-parsed. */\nconst inferBaseType = (value: unknown): BaseType => {\n if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'float';\n if (typeof value === 'boolean') return 'boolean';\n return 'string';\n};\n\nconst inferContainerBaseType = (expression: QtiExpression<unknown>, value: unknown): BaseType | undefined => {\n if (!Array.isArray(value) || value.length > 0) {\n return undefined;\n }\n\n return expression\n .lastVariables\n .find(variable => variable?.baseType && variable.baseType !== 'record' && variable.cardinality !== 'record')\n ?.baseType as BaseType | undefined;\n};\n\nexport abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {\n /*\n * Not `@state()`. Nothing renders it any more (see `render` below), so making\n * it reactive only scheduled an update per `calculate()` — and response\n * processing calls that on every expression of every rule it walks.\n */\n protected result: any;\n #lastVariables: ExpressionVariable[] = [];\n\n /**\n * What the last `calculate()` produced, or undefined if it has not run.\n *\n * This is the supported way to inspect a scoring run: process the responses,\n * then walk the rule tree reading this off each expression. A tool that wants\n * to visualise how a score came about gets the whole tree annotated with its\n * values, without the components having to re-render to show them.\n */\n public get lastResult(): Readonly<T> | undefined {\n return this.result;\n }\n\n public get lastVariables(): readonly ExpressionVariable[] {\n return this.#lastVariables;\n }\n\n /*\n * Written here rather than imported from a .css file, and that is a packaging constraint rather\n * than a style preference.\n *\n * The rule used to live in qti-expression.css, imported with Vite's inline-stylesheet query so\n * DevTools could show source-file provenance. That query is a VITE specifier, and tsc copies it\n * verbatim into dist — so the published package shipped an import only a Vite-based consumer\n * could resolve. Every other consumer broke on it: plain Node, tsx, any non-Vite bundler. It was\n * the single such specifier reachable from this package, and it took down QTI-Editor's schema\n * check, which loads the components under tsx to build the real ProseMirror schema\n * (ERR_UNKNOWN_FILE_EXTENSION on a file that has nothing to do with schemas).\n *\n * A `build:assets` step used to copy the .css into dist beside it. That went too — it existed\n * only to give the unresolvable import something to point at.\n *\n * Two lines of CSS are not worth a dist that only one bundler can consume. If this ever grows\n * into a real stylesheet, give it a `.styles.ts` exporting a `css` template — the house pattern\n * everywhere else in this repo — rather than reintroducing a bundler-specific import.\n *\n * The element renders a debug <pre> of its computed result plus a <slot>; hiding the slot is all\n * the styling it has ever had.\n */\n static override styles = css`\n slot {\n display: none;\n }\n `;\n\n /*\n * Just the slot. This used to render `JSON.stringify(this.result, null, 2)`\n * into a <pre> beside it — a development aid that nobody could see, because\n * expressions live inside `qti-response-processing`, which is `display: none`.\n * It cost a full serialisation of the result on every render.\n */\n override render() {\n return html`<slot></slot>`;\n }\n\n /*\n * Render once, then never again: the template above is constant, so every\n * later update is pure overhead.\n *\n * It is not the `@state()` decorators that drive those updates — `@consume`\n * subscribes through `ContextConsumer`, which calls `host.requestUpdate()`\n * itself on every context change, \"in case this value is used in a template\".\n * Here it is not. An item with 30 response-processing rules holds ~180\n * expression elements and the test context changes on every keystroke, so\n * that was ~180 renders per keystroke producing identical output.\n *\n * The subscriptions stay, because `calculate()` reads the current values when\n * a rule invokes it. Only the rendering stops.\n */\n protected override shouldUpdate(): boolean {\n return !this.hasUpdated;\n }\n\n public calculate(): Readonly<T> {\n this.result = this.getResult();\n return this.result;\n }\n\n protected getResult(): Readonly<T> {\n throw new Error('Not implemented');\n }\n\n /**\n * The base type this operator always produces, when the QTI vocabulary fixes\n * one regardless of its operands — `qti-divide` is a float even dividing two\n * integers exactly, and `qti-round` is an integer whatever it rounds.\n *\n * Left undefined by operators whose result type follows their operands (a\n * `qti-sum` of integers is an integer) and by those that return no number at\n * all; the type is then read off the value.\n */\n public get resultBaseType(): BaseType | undefined {\n return undefined;\n }\n\n /*\n * Subscribed but deliberately NOT `@state()`.\n *\n * An expression is pull-based: nothing reads these until a rule calls\n * `calculate()`, which reads whatever the consumer has by then. Marking them\n * reactive made every context change schedule a render of every expression\n * element in the document — an item with 30 response-processing rules holds\n * ~180 of them, and the test context changes on every keystroke. The\n * subscription still keeps the properties current; it just no longer asks\n * for a render nothing depends on.\n */\n @consume({ context: itemContext, subscribe: true })\n protected context?: ItemContext;\n\n @consume({ context: qtiContext, subscribe: true })\n protected qtiContext?: QtiContext;\n\n @consume({ context: testContext, subscribe: true })\n protected _testContext?: TestContext;\n\n /**\n * Resolve a declared variable from the closest available scope: item-level\n * first, then the test-level outcome variables. So an expression used in an\n * assessmentTest's outcome processing can reference a test-level outcome\n * (e.g. a total set earlier in the same run) — which the QTI variable\n * expression is specified to do. Returns null when unresolved rather than\n * throwing, so callers degrade gracefully.\n */\n protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null {\n return (\n this.context?.variables.find(v => v.identifier === identifier) ??\n this._testContext?.testOutcomeVariables?.find(v => v.identifier === identifier) ??\n null\n );\n }\n\n getVariables = (): ExpressionVariable[] => {\n // FIXME: if this itself is multiple, this will never enter the qti-multiple switch\n // See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml\n\n const variables = Array.from(this.children)\n .map((e: Element) => {\n switch (e.tagName.toLowerCase()) {\n case 'qti-base-value': {\n return {\n baseType: e.getAttribute('base-type'),\n value: e.textContent.trim(),\n cardinality: 'single'\n } as ResponseVariable;\n }\n case 'qti-variable': {\n const identifier = e.getAttribute('identifier') || '';\n\n // Special handling for QTI_CONTEXT\n if (identifier === 'QTI_CONTEXT') {\n if (!this.qtiContext?.QTI_CONTEXT) {\n console.warn('QTI_CONTEXT not available');\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n\n return this.resolveVariable(identifier);\n }\n case 'qti-multiple': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-ordered': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'ordered',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-repeat': {\n const repeat = e as QtiExpression<ResponseVariable[]>;\n const values = repeat.getResult();\n if (values?.length > 0) {\n return values;\n }\n return null;\n }\n case 'qti-custom-operator': {\n // Not a QtiExpression — it has `calculate` but no `getResult`, so\n // the default branch below throws on it and the surrounding\n // expression is handed nothing.\n return variableFromValue((e as unknown as Calculate).calculate?.());\n }\n case 'qti-correct': {\n const identifier = e.getAttribute('identifier') || '';\n const responseVariable = this.context?.variables?.find(v => v.identifier === identifier) as\n | ResponseVariable\n | undefined;\n\n // A `qti-correct` naming a response that is not declared — a typo,\n // or an expression evaluated before the declaration registered —\n // used to dereference the null this deliberately produced and take\n // down the whole processing run. It is an unresolved variable, so\n // it is NULL.\n if (!responseVariable) {\n console.warn(`qti-correct: no response declaration for \"${identifier}\"`);\n return variableFromValue(null);\n }\n\n return {\n baseType: responseVariable.baseType,\n value: responseVariable.correctResponse,\n cardinality: responseVariable.cardinality\n } as ResponseVariable;\n }\n default: {\n // Every operator without a case of its own lands here.\n try {\n const expression = e as QtiExpression<unknown>;\n const result = expression.getResult();\n return variableFromValue(result, expression.resultBaseType ?? inferContainerBaseType(expression, result));\n } catch (error) {\n console.warn(`getVariables: could not read a value from <${e.tagName.toLowerCase()}>`, error);\n }\n return null;\n }\n }\n })\n .flatMap(v => (Array.isArray(v) ? v : [v]))\n .filter((v): v is ExpressionVariable => v !== null);\n\n this.#lastVariables = variables;\n return variables;\n };\n}\n"]}
|
|
@@ -5,8 +5,7 @@ export declare abstract class QtiFeedback extends LitElement {
|
|
|
5
5
|
outcomeIdentifier: string;
|
|
6
6
|
protected identifier: string;
|
|
7
7
|
showStatus: string;
|
|
8
|
-
private _context
|
|
8
|
+
private get _context();
|
|
9
9
|
private _computedContext?;
|
|
10
|
-
connectedCallback(): void;
|
|
11
10
|
checkShowFeedback(outcomeIdentifier: string): void;
|
|
12
11
|
}
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { consume } from '@lit/context';
|
|
7
|
+
import { ContextConsumer, consume } from '@lit/context';
|
|
8
8
|
import { LitElement } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { computedContext } from '../context/computed.context';
|
|
@@ -14,14 +14,45 @@ export class QtiFeedback extends LitElement {
|
|
|
14
14
|
constructor() {
|
|
15
15
|
super(...arguments);
|
|
16
16
|
this.showHide = 'show';
|
|
17
|
+
/**
|
|
18
|
+
* The item's variables. Feedback follows them itself: every time the item publishes a new context
|
|
19
|
+
* this decides, synchronously, whether its own outcome now calls for showing it, so the item has
|
|
20
|
+
* no list of feedback elements to push to. Synchronous matters. A host that sets an outcome
|
|
21
|
+
* reads `showStatus` straight after, with no await in between.
|
|
22
|
+
*/
|
|
23
|
+
this.#itemContext = new ContextConsumer(this, {
|
|
24
|
+
context: itemContext,
|
|
25
|
+
subscribe: true,
|
|
26
|
+
callback: context => this.#followItem(context)
|
|
27
|
+
});
|
|
17
28
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The item's variables. Feedback follows them itself: every time the item publishes a new context
|
|
31
|
+
* this decides, synchronously, whether its own outcome now calls for showing it, so the item has
|
|
32
|
+
* no list of feedback elements to push to. Synchronous matters. A host that sets an outcome
|
|
33
|
+
* reads `showStatus` straight after, with no await in between.
|
|
34
|
+
*/
|
|
35
|
+
#itemContext;
|
|
36
|
+
get _context() {
|
|
37
|
+
return this.#itemContext.value;
|
|
38
|
+
}
|
|
39
|
+
/** What the last context carried that decides this feedback, to tell a relevant update from noise. */
|
|
40
|
+
#seen;
|
|
41
|
+
/**
|
|
42
|
+
* Re-decide when this feedback's outcome changed, or once an attempt has been made. Before the
|
|
43
|
+
* first attempt an unchanged outcome says nothing yet: a `hide` feedback would otherwise switch
|
|
44
|
+
* on for an item nobody has answered.
|
|
45
|
+
*/
|
|
46
|
+
#followItem(context) {
|
|
47
|
+
const variables = context?.variables;
|
|
48
|
+
if (!variables || !this.outcomeIdentifier)
|
|
49
|
+
return;
|
|
50
|
+
const outcome = JSON.stringify(variables.find(v => v.identifier === this.outcomeIdentifier)?.value ?? null);
|
|
51
|
+
const numAttempts = Number(variables.find(v => v.identifier === 'numAttempts')?.value) || 0;
|
|
52
|
+
const relevant = this.#seen === undefined ? numAttempts > 0 : this.#seen !== outcome || numAttempts > 0;
|
|
53
|
+
this.#seen = outcome;
|
|
54
|
+
if (relevant)
|
|
55
|
+
this.checkShowFeedback(this.outcomeIdentifier);
|
|
25
56
|
}
|
|
26
57
|
checkShowFeedback(outcomeIdentifier) {
|
|
27
58
|
const outcomeVariable = this._context.variables.find(v => v.identifier === outcomeIdentifier) || null;
|
|
@@ -95,10 +126,6 @@ __decorate([
|
|
|
95
126
|
__decorate([
|
|
96
127
|
property({ type: String, attribute: false })
|
|
97
128
|
], QtiFeedback.prototype, "showStatus", void 0);
|
|
98
|
-
__decorate([
|
|
99
|
-
consume({ context: itemContext, subscribe: true }),
|
|
100
|
-
state()
|
|
101
|
-
], QtiFeedback.prototype, "_context", void 0);
|
|
102
129
|
__decorate([
|
|
103
130
|
consume({ context: computedContext, subscribe: true }),
|
|
104
131
|
state()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qti-feedback.js","sourceRoot":"","sources":["../../src/abstract/qti-feedback.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"qti-feedback.js","sourceRoot":"","sources":["../../src/abstract/qti-feedback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextConsumer, consume } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { computedContext } from '../context/computed.context';\nimport { itemContext } from '../context/item.context';\nimport { IsNullOrUndefined } from '../utils/utils';\n\nimport type { ComputedContext } from '../context/types/computed.types';\nimport type { ItemContext } from '../context/types/item.types';\n\nexport abstract class QtiFeedback extends LitElement {\n @property({ type: String, attribute: 'show-hide' })\n protected showHide: 'show' | 'hide' = 'show';\n\n @property({ type: String, attribute: 'outcome-identifier' })\n public outcomeIdentifier: string;\n\n @property({ type: String })\n protected identifier: string;\n\n @property({ type: String, attribute: false })\n public showStatus: string;\n\n /**\n * The item's variables. Feedback follows them itself: every time the item publishes a new context\n * this decides, synchronously, whether its own outcome now calls for showing it, so the item has\n * no list of feedback elements to push to. Synchronous matters. A host that sets an outcome\n * reads `showStatus` straight after, with no await in between.\n */\n #itemContext = new ContextConsumer(this, {\n context: itemContext,\n subscribe: true,\n callback: context => this.#followItem(context)\n });\n\n private get _context(): ItemContext | undefined {\n return this.#itemContext.value;\n }\n\n /** What the last context carried that decides this feedback, to tell a relevant update from noise. */\n #seen?: string;\n\n @consume({ context: computedContext, subscribe: true })\n @state()\n private _computedContext?: ComputedContext;\n\n /**\n * Re-decide when this feedback's outcome changed, or once an attempt has been made. Before the\n * first attempt an unchanged outcome says nothing yet: a `hide` feedback would otherwise switch\n * on for an item nobody has answered.\n */\n #followItem(context: ItemContext | undefined) {\n const variables = context?.variables;\n if (!variables || !this.outcomeIdentifier) return;\n const outcome = JSON.stringify(variables.find(v => v.identifier === this.outcomeIdentifier)?.value ?? null);\n const numAttempts = Number(variables.find(v => v.identifier === 'numAttempts')?.value) || 0;\n const relevant = this.#seen === undefined ? numAttempts > 0 : this.#seen !== outcome || numAttempts > 0;\n this.#seen = outcome;\n if (relevant) this.checkShowFeedback(this.outcomeIdentifier);\n }\n\n public checkShowFeedback(outcomeIdentifier: string) {\n const outcomeVariable = this._context.variables.find(v => v.identifier === outcomeIdentifier) || null;\n if (this.outcomeIdentifier !== outcomeIdentifier || !outcomeVariable) return;\n let isFound = false;\n if (Array.isArray(outcomeVariable.value)) {\n isFound = outcomeVariable.value.includes(this.identifier);\n } else {\n isFound =\n (!IsNullOrUndefined(this.identifier) &&\n !IsNullOrUndefined(outcomeVariable?.value) &&\n this.identifier === outcomeVariable.value) ||\n false;\n }\n\n this.#showFeedback(isFound);\n }\n\n #showFeedback(value: boolean) {\n if (!this.#sessionControlAllowsFeedback()) {\n this.showStatus = 'off';\n return;\n }\n this.showStatus = (value && this.showHide === 'show') || (!value && this.showHide === 'hide') ? 'on' : 'off';\n }\n\n /**\n * The QTI `show-feedback` constraint governs exactly one state: after the end of\n * the last attempt. Up to that point the spec requires any applicable feedback to\n * be shown — \"a value of max-attempts greater than 1, by definition, indicates\n * that any applicable feedback must be shown\" — and only \"once the maximum number\n * of allowed attempts have been used (or for adaptive items, completionStatus has\n * been set to completed)\" does `show-feedback` decide.\n *\n * This only ever forces feedback off, so `show-feedback` being true settles it:\n * there is no state in which the constraint hides feedback. It defaults to\n * false, and an absent value suppresses once the attempts are gone.\n *\n * That leaves one question — is the item out of attempts? — and the two item\n * kinds answer it from different variables, so each reads only its own.\n */\n #sessionControlAllowsFeedback(): boolean {\n if (!this._computedContext) return true;\n const activeItem = this._computedContext.testParts\n ?.find(tp => tp.active)\n ?.sections.flatMap(s => s.items)\n .find(i => i.active);\n if (!activeItem || activeItem.showFeedback) return true;\n\n // Adaptive items ignore max-attempts: \"the number of attempts is limited by\n // the value of the completionStatus built-in outcome variable\".\n if (activeItem.adaptive) return activeItem.completionStatus !== 'completed';\n\n // `max-attempts=\"0\"` is \"no limit\", so the last attempt never arrives.\n const maxAttempts = activeItem.maxAttempts ?? 1;\n if (maxAttempts <= 0) return true;\n\n const numAttempts = Number(this._context?.variables?.find(v => v.identifier === 'numAttempts')?.value) || 0;\n return numAttempts < maxAttempts;\n }\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catches context requests nobody answered and asks again when a provider connects, so a component
|
|
3
|
+
* that upgrades before the element providing its context still gets the value.
|
|
4
|
+
*
|
|
5
|
+
* It has to live on the document and not on one of our elements: the case it exists for is the
|
|
6
|
+
* providing element not having upgraded yet, so there is nothing of ours to attach it to. It holds
|
|
7
|
+
* only requests that subscribe, which every `@consume` in this library does, and it holds them
|
|
8
|
+
* weakly, so nothing is kept alive that would not be anyway.
|
|
9
|
+
*
|
|
10
|
+
* Once per page, however many copies of the package the page ends up with.
|
|
11
|
+
*/
|
|
12
|
+
export declare const installContextRoot: () => void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ContextRoot } from '@lit/context';
|
|
2
|
+
const INSTALLED = Symbol.for('qti-components.context-root');
|
|
3
|
+
/**
|
|
4
|
+
* Catches context requests nobody answered and asks again when a provider connects, so a component
|
|
5
|
+
* that upgrades before the element providing its context still gets the value.
|
|
6
|
+
*
|
|
7
|
+
* It has to live on the document and not on one of our elements: the case it exists for is the
|
|
8
|
+
* providing element not having upgraded yet, so there is nothing of ours to attach it to. It holds
|
|
9
|
+
* only requests that subscribe, which every `@consume` in this library does, and it holds them
|
|
10
|
+
* weakly, so nothing is kept alive that would not be anyway.
|
|
11
|
+
*
|
|
12
|
+
* Once per page, however many copies of the package the page ends up with.
|
|
13
|
+
*/
|
|
14
|
+
export const installContextRoot = () => {
|
|
15
|
+
if (typeof document === 'undefined')
|
|
16
|
+
return;
|
|
17
|
+
const page = globalThis;
|
|
18
|
+
if (page[INSTALLED])
|
|
19
|
+
return;
|
|
20
|
+
page[INSTALLED] = new ContextRoot();
|
|
21
|
+
page[INSTALLED].attach(document.documentElement);
|
|
22
|
+
};
|
|
23
|
+
installContextRoot();
|
|
24
|
+
//# sourceMappingURL=context-root.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-root.js","sourceRoot":"","sources":["../../src/context/context-root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAS,EAAE;IAC3C,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAC5C,MAAM,IAAI,GAAG,UAAgE,CAAC;IAC9E,IAAI,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO;IAC5B,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,WAAW,EAAE,CAAC;IACpC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;AACnD,CAAC,CAAC;AAEF,kBAAkB,EAAE,CAAC","sourcesContent":["import { ContextRoot } from '@lit/context';\n\nconst INSTALLED = Symbol.for('qti-components.context-root');\n\n/**\n * Catches context requests nobody answered and asks again when a provider connects, so a component\n * that upgrades before the element providing its context still gets the value.\n *\n * It has to live on the document and not on one of our elements: the case it exists for is the\n * providing element not having upgraded yet, so there is nothing of ours to attach it to. It holds\n * only requests that subscribe, which every `@consume` in this library does, and it holds them\n * weakly, so nothing is kept alive that would not be anyway.\n *\n * Once per page, however many copies of the package the page ends up with.\n */\nexport const installContextRoot = (): void => {\n if (typeof document === 'undefined') return;\n const page = globalThis as unknown as Record<symbol, ContextRoot | undefined>;\n if (page[INSTALLED]) return;\n page[INSTALLED] = new ContextRoot();\n page[INSTALLED].attach(document.documentElement);\n};\n\ninstallContextRoot();\n"]}
|